fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선
- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산 - 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader) - 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql) - synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
22
synology_deployment/api/controllers/analysisController.js
Normal file
22
synology_deployment/api/controllers/analysisController.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// /controllers/analysisController.js
|
||||
const analysisService = require('../services/analysisService');
|
||||
|
||||
/**
|
||||
* 프로젝트 분석 데이터를 조회하는 API 요청을 처리합니다.
|
||||
*/
|
||||
const getAnalysisData = async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const data = await analysisService.getAnalysisService(startDate, endDate);
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error('💥 분석 데이터 컨트롤러 오류:', err);
|
||||
res.status(400).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAnalysisData,
|
||||
};
|
||||
306
synology_deployment/api/controllers/attendanceController.js
Normal file
306
synology_deployment/api/controllers/attendanceController.js
Normal file
@@ -0,0 +1,306 @@
|
||||
const AttendanceModel = require('../models/attendanceModel');
|
||||
|
||||
class AttendanceController {
|
||||
// 일일 근태 현황 조회 (대시보드용)
|
||||
static async getDailyAttendanceStatus(req, res) {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const attendanceStatus = await AttendanceModel.getWorkerAttendanceStatus(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: attendanceStatus,
|
||||
message: '근태 현황을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 현황 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 현황 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 일일 근태 기록 조회
|
||||
static async getDailyAttendanceRecords(req, res) {
|
||||
try {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const records = await AttendanceModel.getDailyAttendanceRecords(date, worker_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: records,
|
||||
message: '근태 기록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 기록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 기록 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 근태 기록 생성/업데이트
|
||||
static async upsertAttendanceRecord(req, res) {
|
||||
try {
|
||||
const {
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_vacation_processed,
|
||||
overtime_approved,
|
||||
status,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!record_date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜와 작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const recordData = {
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours: total_work_hours || 0,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_vacation_processed: is_vacation_processed || false,
|
||||
overtime_approved: overtime_approved || false,
|
||||
status: status || 'incomplete',
|
||||
notes,
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
const result = await AttendanceModel.upsertAttendanceRecord(recordData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '근태 기록이 성공적으로 저장되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 기록 저장 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 기록 저장 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 처리
|
||||
static async processVacation(req, res) {
|
||||
try {
|
||||
const { worker_id, date, vacation_type } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !date || !vacation_type) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID, 날짜, 휴가 유형이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 휴가 유형 검증
|
||||
const validVacationTypes = ['ANNUAL_FULL', 'ANNUAL_HALF', 'ANNUAL_QUARTER'];
|
||||
if (!validVacationTypes.includes(vacation_type)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 휴가 유형입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await AttendanceModel.processVacation(
|
||||
worker_id,
|
||||
date,
|
||||
vacation_type,
|
||||
req.user.user_id
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '휴가 처리가 성공적으로 완료되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 처리 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 처리 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 초과근무 승인
|
||||
static async approveOvertime(req, res) {
|
||||
try {
|
||||
const { worker_id, date } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID와 날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await AttendanceModel.approveOvertime(
|
||||
worker_id,
|
||||
date,
|
||||
req.user.user_id
|
||||
);
|
||||
|
||||
if (result) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '초과근무가 성공적으로 승인되었습니다.'
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 날짜의 근태 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('초과근무 승인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '초과근무 승인 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 근로 유형 목록 조회
|
||||
static async getAttendanceTypes(req, res) {
|
||||
try {
|
||||
const attendanceTypes = await AttendanceModel.getAttendanceTypes();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: attendanceTypes,
|
||||
message: '근로 유형 목록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근로 유형 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근로 유형 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 유형 목록 조회
|
||||
static async getVacationTypes(req, res) {
|
||||
try {
|
||||
const vacationTypes = await AttendanceModel.getVacationTypes();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: vacationTypes,
|
||||
message: '휴가 유형 목록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 유형 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 휴가 잔여 조회
|
||||
static async getWorkerVacationBalance(req, res) {
|
||||
try {
|
||||
const { worker_id } = req.params;
|
||||
const { year } = req.query;
|
||||
|
||||
if (!worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const balance = await AttendanceModel.getWorkerVacationBalance(
|
||||
parseInt(worker_id),
|
||||
year ? parseInt(year) : null
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: balance,
|
||||
message: '휴가 잔여 정보를 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 잔여 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔여 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 근태 통계
|
||||
static async getMonthlyAttendanceStats(req, res) {
|
||||
try {
|
||||
const { year, month, worker_id } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도와 월이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const stats = await AttendanceModel.getMonthlyAttendanceStats(
|
||||
parseInt(year),
|
||||
parseInt(month),
|
||||
worker_id ? parseInt(worker_id) : null
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
message: '월별 근태 통계를 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('월별 근태 통계 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 근태 통계 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttendanceController;
|
||||
157
synology_deployment/api/controllers/authController.js
Normal file
157
synology_deployment/api/controllers/authController.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authService = require('../services/auth.service');
|
||||
const { ApiError, asyncHandler } = require('../utils/errorHandler');
|
||||
const { validateSchema, schemas } = require('../utils/validator');
|
||||
|
||||
const login = asyncHandler(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
// 유효성 검사
|
||||
if (!username || !password) {
|
||||
throw new ApiError('사용자명과 비밀번호를 입력해주세요.', 400);
|
||||
}
|
||||
|
||||
const result = await authService.loginService(username, password, ipAddress, userAgent);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ApiError(result.error, result.status || 400);
|
||||
}
|
||||
|
||||
// 로그인 성공 후, 모든 권한을 그룹장 대시보드로 통일
|
||||
const user = result.data.user;
|
||||
const redirectUrl = '/pages/dashboard/group-leader.html'; // 모든 사용자를 그룹장 대시보드로 리다이렉트
|
||||
|
||||
// 새로운 응답 포맷터 사용
|
||||
res.auth(user, result.data.token, redirectUrl, '로그인 성공');
|
||||
});
|
||||
|
||||
// ✅ 사용자 등록 기능 추가
|
||||
exports.register = async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, access_level, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !password || !name || !access_level) {
|
||||
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: '이미 존재하는 아이디입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// role 설정 (access_level에 따라)
|
||||
const roleMap = {
|
||||
'admin': 'admin',
|
||||
'system': 'system', // 시스템 계정은 system role로 설정
|
||||
'group_leader': 'leader',
|
||||
'support_team': 'support',
|
||||
'worker': 'user'
|
||||
};
|
||||
const role = roleMap[access_level] || 'user';
|
||||
|
||||
// 사용자 등록
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Users (username, password, name, role, access_level, worker_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[username, hashedPassword, name, role, access_level, worker_id]
|
||||
);
|
||||
|
||||
console.log('[사용자 등록 성공]', username);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: '사용자 등록이 완료되었습니다.',
|
||||
user_id: result.insertId
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[사용자 등록 오류]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
detail: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 사용자 삭제 기능 추가
|
||||
exports.deleteUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
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: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 삭제
|
||||
await db.query('DELETE FROM Users WHERE user_id = ?', [id]);
|
||||
|
||||
console.log('[사용자 삭제 성공] ID:', id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: '사용자가 삭제되었습니다.'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[사용자 삭제 오류]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
detail: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 사용자 목록 조회
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 비밀번호 제외하고 조회
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
||||
FROM Users
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
res.status(200).json(rows);
|
||||
} catch (err) {
|
||||
console.error('[사용자 목록 조회 실패]', err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
login
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
// /controllers/dailyIssueReportController.js
|
||||
const dailyIssueReportService = require('../services/dailyIssueReportService');
|
||||
|
||||
/**
|
||||
* 1. CREATE: 일일 이슈 보고서 생성 (Service Layer 사용)
|
||||
*/
|
||||
const createDailyIssueReport = async (req, res) => {
|
||||
try {
|
||||
// 프론트엔드에서 worker_ids로 보내주기로 약속함
|
||||
const issueData = { ...req.body, worker_ids: req.body.worker_ids || req.body.worker_id };
|
||||
|
||||
const result = await dailyIssueReportService.createDailyIssueReportService(issueData);
|
||||
|
||||
res.status(201).json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
console.error('💥 이슈 보고서 생성 컨트롤러 오류:', err);
|
||||
res.status(400).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. READ BY DATE: 날짜별 이슈 조회 (Service Layer 사용)
|
||||
*/
|
||||
const getDailyIssuesByDate = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
const issues = await dailyIssueReportService.getDailyIssuesByDateService(date);
|
||||
res.json(issues);
|
||||
} catch (err) {
|
||||
console.error('💥 이슈 보고서 조회 컨트롤러 오류:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. DELETE: 이슈 보고서 삭제 (Service Layer 사용)
|
||||
*/
|
||||
const removeDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const result = await dailyIssueReportService.removeDailyIssueService(id);
|
||||
res.json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
console.error('💥 이슈 보고서 삭제 컨트롤러 오류:', err);
|
||||
const statusCode = err.statusCode || 500;
|
||||
res.status(statusCode).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 레거시 함수들은 더 이상 라우팅되지 않으므로 제거하거나 주석 처리 가능
|
||||
// exports.getDailyIssueById = ...
|
||||
// exports.updateDailyIssue = ...
|
||||
|
||||
module.exports = {
|
||||
createDailyIssueReport,
|
||||
getDailyIssuesByDate,
|
||||
removeDailyIssue,
|
||||
};
|
||||
@@ -0,0 +1,750 @@
|
||||
// controllers/dailyWorkReportController.js - 누적입력 방식 + 모든 기존 기능 포함
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
|
||||
*/
|
||||
const createDailyWorkReport = (req, res) => {
|
||||
const { report_date, worker_id, work_entries } = req.body;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
const created_by_name = req.user?.name || req.user?.username || '알 수 없는 사용자';
|
||||
|
||||
// 1. 기본 유효성 검사
|
||||
if (!report_date || !worker_id || !work_entries) {
|
||||
return res.status(400).json({
|
||||
error: '필수 필드가 누락되었습니다.',
|
||||
required: ['report_date', 'worker_id', 'work_entries'],
|
||||
received: {
|
||||
report_date: !!report_date,
|
||||
worker_id: !!worker_id,
|
||||
work_entries: !!work_entries
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(work_entries) || work_entries.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: '최소 하나의 작업 항목이 필요합니다.',
|
||||
received_entries: work_entries?.length || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 작업 항목 유효성 검사
|
||||
for (let i = 0; i < work_entries.length; i++) {
|
||||
const entry = work_entries[i];
|
||||
const requiredFields = ['project_id', 'work_type_id', 'work_status_id', 'work_hours'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 ${field}가 누락되었습니다.`,
|
||||
entry_index: i,
|
||||
missing_field: field
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 상태인 경우 에러 타입 필수
|
||||
if (entry.work_status_id === 2 && (!entry.error_type_id)) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}이 에러 상태인 경우 error_type_id가 필요합니다.`,
|
||||
entry_index: i
|
||||
});
|
||||
}
|
||||
|
||||
// 시간 유효성 검사
|
||||
const hours = parseFloat(entry.work_hours);
|
||||
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 작업시간이 유효하지 않습니다. (0-24시간)`,
|
||||
entry_index: i,
|
||||
received_hours: entry.work_hours
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 총 시간 계산
|
||||
const total_hours = work_entries.reduce((sum, entry) => sum + (parseFloat(entry.work_hours) || 0), 0);
|
||||
|
||||
// 4. 요청 데이터 구성
|
||||
const reportData = {
|
||||
report_date,
|
||||
worker_id: parseInt(worker_id),
|
||||
work_entries,
|
||||
created_by,
|
||||
created_by_name,
|
||||
total_hours,
|
||||
is_update: false
|
||||
};
|
||||
|
||||
console.log('📝 작업보고서 누적 추가 요청:', {
|
||||
date: report_date,
|
||||
worker: worker_id,
|
||||
creator: created_by_name,
|
||||
creator_id: created_by,
|
||||
entries: work_entries.length,
|
||||
total_hours
|
||||
});
|
||||
|
||||
// 5. 누적 추가 실행 (덮어쓰기 없음!)
|
||||
dailyWorkReportModel.createDailyReport(reportData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 생성 중 오류가 발생했습니다.',
|
||||
details: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 작업보고서 누적 추가 성공:', result);
|
||||
res.status(201).json({
|
||||
message: '작업보고서가 성공적으로 누적 추가되었습니다.',
|
||||
report_date,
|
||||
worker_id,
|
||||
created_by: created_by_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
const getContributorsSummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('기여자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '기여자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
|
||||
|
||||
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
contributors: data,
|
||||
total_contributors: data.length,
|
||||
grand_total_hours: totalHours,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 개인 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getMyAccumulatedData = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
created_by,
|
||||
my_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
|
||||
*/
|
||||
const removeMyEntry = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
|
||||
if (err) {
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '항목 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '항목이 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 작업보고서 조회 (쿼리 파라미터 기반 - 작성자별 필터링 강화)
|
||||
*/
|
||||
const getDailyWorkReports = (req, res) => {
|
||||
const { date, worker_id, created_by: requested_created_by } = req.query;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 일반 사용자는 자신이 작성한 것만 볼 수 있음
|
||||
const created_by = requested_created_by || current_user_id;
|
||||
|
||||
console.log('📊 작업보고서 조회 요청:', {
|
||||
date,
|
||||
worker_id,
|
||||
requested_created_by,
|
||||
current_user_id,
|
||||
final_created_by: created_by
|
||||
});
|
||||
|
||||
if (date && created_by) {
|
||||
// 날짜 + 작성자별 조회
|
||||
dailyWorkReportModel.getByDateAndCreator(date, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
} else if (date && worker_id) {
|
||||
// 기존 방식: 날짜 + 작업자별 (하지만 작성자 필터링 추가)
|
||||
dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜+작업자별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else if (date) {
|
||||
// 날짜별 조회 (작성자 필터링)
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: '날짜(date) 파라미터가 필요합니다.',
|
||||
example: 'date=2024-06-16',
|
||||
optional: ['worker_id', 'created_by']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 날짜별 작업보고서 조회 (경로 파라미터)
|
||||
*/
|
||||
const getDailyWorkReportsByDate = (req, res) => {
|
||||
const { date } = req.params;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜별 조회 (경로): date=${date}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === created_by);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔍 작업보고서 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWorkReports = (req, res) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31',
|
||||
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit']
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id: worker_id ? parseInt(worker_id) : null,
|
||||
project_id: project_id ? parseInt(project_id) : null,
|
||||
work_status_id: work_status_id ? parseInt(work_status_id) : null,
|
||||
created_by, // 작성자 필터링 추가
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🔍 작업보고서 검색 요청:', searchParams);
|
||||
|
||||
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 검색 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 검색 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (작성자별 필터링)
|
||||
*/
|
||||
const getWorkReportStats = (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '통계 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
...data,
|
||||
metadata: {
|
||||
note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정',
|
||||
requested_by: created_by,
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회
|
||||
*/
|
||||
const getDailySummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (date) {
|
||||
console.log(`📊 일일 요약 조회: date=${date}`);
|
||||
dailyWorkReportModel.getSummaryByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('일일 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '일일 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else if (worker_id) {
|
||||
console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`);
|
||||
dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'date 또는 worker_id 파라미터가 필요합니다.',
|
||||
examples: [
|
||||
'date=2024-06-16',
|
||||
'worker_id=1'
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📅 월간 요약 조회
|
||||
*/
|
||||
const getMonthlySummary = (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
error: 'year와 month가 필요합니다.',
|
||||
example: 'year=2024&month=01',
|
||||
note: 'month는 01, 02, ..., 12 형식으로 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월간 요약 조회: ${year}-${month}`);
|
||||
|
||||
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
|
||||
if (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '월간 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
summary: data,
|
||||
total_entries: data.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✏️ 작업보고서 수정
|
||||
*/
|
||||
const updateWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
const updated_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!updated_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_by = updated_by;
|
||||
|
||||
console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`);
|
||||
|
||||
dailyWorkReportModel.updateById(id, updateData, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 수정 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '수정할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 수정 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 수정되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
updated_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제
|
||||
*/
|
||||
const removeDailyWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeById(id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
date: date,
|
||||
worker_id: worker_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`);
|
||||
res.json({
|
||||
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
|
||||
date,
|
||||
worker_id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getWorkTypes = (req, res) => {
|
||||
console.log('📋 작업 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkStatusTypes = (req, res) => {
|
||||
console.log('📋 업무 상태 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorTypes = (req, res) => {
|
||||
console.log('📋 에러 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllErrorTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '에러 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 에러 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (기존 기능 + 누적 기능)
|
||||
module.exports = {
|
||||
// 📝 핵심 CRUD 함수들
|
||||
createDailyWorkReport, // 누적 추가 (덮어쓰기 없음)
|
||||
getDailyWorkReports, // 조회 (작성자별 필터링)
|
||||
getDailyWorkReportsByDate, // 날짜별 조회
|
||||
searchWorkReports, // 검색 (페이지네이션)
|
||||
updateWorkReport, // 수정
|
||||
removeDailyWorkReport, // 개별 삭제
|
||||
removeDailyWorkReportByDateAndWorker, // 전체 삭제
|
||||
|
||||
// 🔄 누적 관련 새로운 함수들
|
||||
getAccumulatedReports, // 누적 현황 조회
|
||||
getContributorsSummary, // 기여자별 요약
|
||||
getMyAccumulatedData, // 개인 누적 현황
|
||||
removeMyEntry, // 개별 항목 삭제 (본인 것만)
|
||||
|
||||
// 📊 요약 및 통계 함수들
|
||||
getDailySummary, // 일일 요약
|
||||
getMonthlySummary, // 월간 요약
|
||||
getWorkReportStats, // 통계
|
||||
|
||||
// 📋 마스터 데이터 함수들
|
||||
getWorkTypes, // 작업 유형 목록
|
||||
getWorkStatusTypes, // 업무 상태 유형 목록
|
||||
getErrorTypes // 에러 유형 목록
|
||||
};
|
||||
831
synology_deployment/api/controllers/dailyWorkReportController.js
Normal file
831
synology_deployment/api/controllers/dailyWorkReportController.js
Normal file
@@ -0,0 +1,831 @@
|
||||
// controllers/dailyWorkReportController.js - 권한별 전체 조회 지원 버전
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
const dailyWorkReportService = require('../services/dailyWorkReportService');
|
||||
const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler');
|
||||
const { validateSchema, schemas } = require('../utils/validator');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const createDailyWorkReport = asyncHandler(async (req, res) => {
|
||||
const reportData = {
|
||||
...req.body,
|
||||
created_by: req.user?.user_id || req.user?.id,
|
||||
created_by_name: req.user?.name || req.user?.username || '알 수 없는 사용자'
|
||||
};
|
||||
|
||||
console.log('🔍 Controller에서 받은 데이터:', JSON.stringify(reportData, null, 2));
|
||||
|
||||
try {
|
||||
const result = await dailyWorkReportService.createDailyWorkReportService(reportData);
|
||||
|
||||
res.created(result, '작업보고서가 성공적으로 생성되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('💥 작업보고서 생성 컨트롤러 오류:', error.message);
|
||||
throw new ApiError('작업보고서 생성에 실패했습니다.', 400);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
const getContributorsSummary = asyncHandler(async (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
throw new ApiError('date와 worker_id가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
try {
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
|
||||
|
||||
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
|
||||
|
||||
const result = {
|
||||
date,
|
||||
worker_id,
|
||||
contributors: data,
|
||||
total_contributors: data.length,
|
||||
grand_total_hours: totalHours
|
||||
};
|
||||
|
||||
res.success(result, '기여자별 요약 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '기여자별 요약 조회');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📊 개인 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getMyAccumulatedData = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
created_by,
|
||||
my_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
|
||||
*/
|
||||
const removeMyEntry = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
|
||||
if (err) {
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '항목 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '항목이 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 작업보고서 조회 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const getDailyWorkReports = async (req, res) => {
|
||||
try {
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
role: req.user?.role || 'user' // 기본값을 'user'로 설정하여 안전하게 처리
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
const reports = await dailyWorkReportService.getDailyWorkReportsService(req.query, userInfo);
|
||||
|
||||
res.json(reports);
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 작업보고서 조회 컨트롤러 오류:', error.message);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: '작업보고서 조회에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원)
|
||||
*/
|
||||
const getDailyWorkReportsByDate = (req, res) => {
|
||||
const { date } = req.params;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
const user_access_level = req.user?.access_level;
|
||||
const user_job_type = req.user?.job_type;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const isAdmin = user_access_level === 'system' || user_access_level === 'admin' || user_access_level === 'leader' || user_job_type === 'leader';
|
||||
|
||||
console.log(`📊 날짜별 조회 (경로): date=${date}, user=${current_user_id}, 권한=${user_access_level}, 직책=${user_job_type}, 관리자=${isAdmin}`);
|
||||
console.log(`🔍 사용자 정보 상세:`, req.user);
|
||||
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 🎯 권한별 필터링 (임시로 비활성화)
|
||||
let finalData = data;
|
||||
console.log(`📊 임시로 모든 사용자에게 전체 조회 허용: ${data.length}개`);
|
||||
console.log(`📊 권한 정보: access_level=${user_access_level}, job_type=${user_job_type}, isAdmin=${isAdmin}`);
|
||||
|
||||
// if (!isAdmin) {
|
||||
// finalData = data.filter(report => report.created_by === current_user_id);
|
||||
// console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`);
|
||||
// } else {
|
||||
// console.log(`📊 관리자 권한으로 전체 조회: ${data.length}개`);
|
||||
// }
|
||||
|
||||
res.json(finalData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔍 작업보고서 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWorkReports = (req, res) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31',
|
||||
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit']
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id: worker_id ? parseInt(worker_id) : null,
|
||||
project_id: project_id ? parseInt(project_id) : null,
|
||||
work_status_id: work_status_id ? parseInt(work_status_id) : null,
|
||||
created_by, // 작성자 필터링 추가
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🔍 작업보고서 검색 요청:', searchParams);
|
||||
|
||||
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 검색 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 검색 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const getWorkReportStats = async (req, res) => {
|
||||
try {
|
||||
const statsData = await dailyWorkReportService.getStatisticsService(req.query);
|
||||
res.json(statsData);
|
||||
} catch (error) {
|
||||
console.error('💥 통계 조회 컨트롤러 오류:', error.message);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: '통계 조회에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const getDailySummary = async (req, res) => {
|
||||
try {
|
||||
const summaryData = await dailyWorkReportService.getSummaryService(req.query);
|
||||
res.json(summaryData);
|
||||
} catch (error) {
|
||||
console.error('💥 일일 요약 조회 컨트롤러 오류:', error.message);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: '일일 요약 조회에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📅 월간 요약 조회
|
||||
*/
|
||||
const getMonthlySummary = (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
error: 'year와 month가 필요합니다.',
|
||||
example: 'year=2024&month=01',
|
||||
note: 'month는 01, 02, ..., 12 형식으로 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월간 요약 조회: ${year}-${month}`);
|
||||
|
||||
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
|
||||
if (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '월간 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
summary: data,
|
||||
total_entries: data.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✏️ 작업보고서 수정 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const updateWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id: reportId } = req.params;
|
||||
const updateData = req.body;
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
role: req.user?.role || 'user'
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
const result = await dailyWorkReportService.updateWorkReportService(reportId, updateData, userInfo);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`💥 작업보고서 수정 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
|
||||
const statusCode = error.statusCode || 400;
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: '작업보고서 수정에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const removeDailyWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id: reportId } = req.params;
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
const result = await dailyWorkReportService.removeDailyWorkReportService(reportId, userInfo);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`💥 작업보고서 삭제 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
|
||||
const statusCode = error.statusCode || 400;
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: '작업보고서 삭제에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <20><>️ 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
date: date,
|
||||
worker_id: worker_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`);
|
||||
res.json({
|
||||
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
|
||||
date,
|
||||
worker_id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getWorkTypes = (req, res) => {
|
||||
console.log('📋 작업 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkStatusTypes = (req, res) => {
|
||||
console.log('📋 업무 상태 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorTypes = (req, res) => {
|
||||
console.log('📋 에러 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllErrorTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '에러 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 에러 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 작업 유형 CRUD ==========
|
||||
|
||||
/**
|
||||
* 📝 작업 유형 생성
|
||||
*/
|
||||
const createWorkType = asyncHandler(async (req, res) => {
|
||||
const { name, description, category } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new ApiError('작업 유형 이름이 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('📝 작업 유형 생성:', { name, description, category });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.createWorkType({ name, description, category }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.created(result, '작업 유형이 성공적으로 생성되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 유형 생성');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✏️ 작업 유형 수정
|
||||
*/
|
||||
const updateWorkType = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, description, category } = req.body;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('작업 유형 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('✏️ 작업 유형 수정:', { id, name, description, category });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.updateWorkType(id, { name, description, category }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('수정할 작업 유형을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '작업 유형이 성공적으로 수정되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 유형 수정');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 🗑️ 작업 유형 삭제
|
||||
*/
|
||||
const deleteWorkType = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('작업 유형 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('🗑️ 작업 유형 삭제:', id);
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.deleteWorkType(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('삭제할 작업 유형을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '작업 유형이 성공적으로 삭제되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 유형 삭제');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 작업 상태 CRUD ==========
|
||||
|
||||
/**
|
||||
* 📝 작업 상태 생성
|
||||
*/
|
||||
const createWorkStatus = asyncHandler(async (req, res) => {
|
||||
const { name, description, is_error } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new ApiError('작업 상태 이름이 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('📝 작업 상태 생성:', { name, description, is_error });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.createWorkStatus({ name, description, is_error }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.created(result, '작업 상태가 성공적으로 생성되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 상태 생성');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✏️ 작업 상태 수정
|
||||
*/
|
||||
const updateWorkStatus = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, description, is_error } = req.body;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('작업 상태 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('✏️ 작업 상태 수정:', { id, name, description, is_error });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.updateWorkStatus(id, { name, description, is_error }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('수정할 작업 상태를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '작업 상태가 성공적으로 수정되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 상태 수정');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 🗑️ 작업 상태 삭제
|
||||
*/
|
||||
const deleteWorkStatus = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('작업 상태 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('🗑️ 작업 상태 삭제:', id);
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.deleteWorkStatus(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('삭제할 작업 상태를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '작업 상태가 성공적으로 삭제되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업 상태 삭제');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 오류 유형 CRUD ==========
|
||||
|
||||
/**
|
||||
* 📝 오류 유형 생성
|
||||
*/
|
||||
const createErrorType = asyncHandler(async (req, res) => {
|
||||
const { name, description, severity } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new ApiError('오류 유형 이름이 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('📝 오류 유형 생성:', { name, description, severity });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.createErrorType({ name, description, severity }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.created(result, '오류 유형이 성공적으로 생성되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '오류 유형 생성');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✏️ 오류 유형 수정
|
||||
*/
|
||||
const updateErrorType = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, description, severity } = req.body;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('오류 유형 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('✏️ 오류 유형 수정:', { id, name, description, severity });
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.updateErrorType(id, { name, description, severity }, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('수정할 오류 유형을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '오류 유형이 성공적으로 수정되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '오류 유형 수정');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 🗑️ 오류 유형 삭제
|
||||
*/
|
||||
const deleteErrorType = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new ApiError('오류 유형 ID가 필요합니다.', 400);
|
||||
}
|
||||
|
||||
console.log('🗑️ 오류 유형 삭제:', id);
|
||||
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
dailyWorkReportModel.deleteErrorType(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('삭제할 오류 유형을 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
res.success(result, '오류 유형이 성공적으로 삭제되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '오류 유형 삭제');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (리팩토링된 함수 위주로 재구성)
|
||||
module.exports = {
|
||||
// 📝 V2 핵심 CRUD 함수
|
||||
createDailyWorkReport,
|
||||
getDailyWorkReports,
|
||||
updateWorkReport,
|
||||
removeDailyWorkReport,
|
||||
|
||||
// 📊 V2 통계 및 요약 함수
|
||||
getWorkReportStats,
|
||||
getDailySummary,
|
||||
|
||||
// 🔽 아직 리팩토링되지 않은 레거시 함수들
|
||||
getAccumulatedReports,
|
||||
getContributorsSummary,
|
||||
getMyAccumulatedData,
|
||||
removeMyEntry,
|
||||
getDailyWorkReportsByDate,
|
||||
searchWorkReports,
|
||||
getMonthlySummary,
|
||||
removeDailyWorkReportByDateAndWorker,
|
||||
getWorkTypes,
|
||||
getWorkStatusTypes,
|
||||
getErrorTypes,
|
||||
|
||||
// 🔽 마스터 데이터 CRUD
|
||||
createWorkType,
|
||||
updateWorkType,
|
||||
deleteWorkType,
|
||||
createWorkStatus,
|
||||
updateWorkStatus,
|
||||
deleteWorkStatus,
|
||||
createErrorType,
|
||||
updateErrorType,
|
||||
deleteErrorType
|
||||
};
|
||||
55
synology_deployment/api/controllers/issueTypeController.js
Normal file
55
synology_deployment/api/controllers/issueTypeController.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const issueTypeModel = require('../models/issueTypeModel');
|
||||
|
||||
exports.createIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.create(req.body, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, issue_type_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAllIssueTypes = async (_req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.getAll((err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.update(id, req.body, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Not found or no changes' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.removeIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
201
synology_deployment/api/controllers/monthlyStatusController.js
Normal file
201
synology_deployment/api/controllers/monthlyStatusController.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// controllers/monthlyStatusController.js
|
||||
// 월별 작업자 상태 집계 컨트롤러
|
||||
|
||||
const MonthlyStatusModel = require('../models/monthlyStatusModel');
|
||||
|
||||
class MonthlyStatusController {
|
||||
// 월별 캘린더 데이터 조회
|
||||
static async getMonthlyCalendarData(req, res) {
|
||||
try {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도(year)와 월(month)이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
const monthNum = parseInt(month);
|
||||
|
||||
if (yearNum < 2020 || yearNum > 2030 || monthNum < 1 || monthNum > 12) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 연도 또는 월입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월별 캘린더 데이터 조회: ${year}년 ${month}월`);
|
||||
|
||||
const summaryData = await MonthlyStatusModel.getMonthlySummary(yearNum, monthNum);
|
||||
|
||||
// 날짜별 객체로 변환 (날짜 키를 YYYY-MM-DD 형식으로 변환)
|
||||
const calendarData = {};
|
||||
summaryData.forEach(day => {
|
||||
const dateKey = day.date.toISOString().split('T')[0]; // YYYY-MM-DD 형식으로 변환
|
||||
calendarData[dateKey] = {
|
||||
totalWorkers: day.total_workers,
|
||||
workingWorkers: day.working_workers,
|
||||
hasIssues: day.has_issues,
|
||||
hasErrors: day.has_errors,
|
||||
hasOvertimeWarning: day.has_overtime_warning,
|
||||
incompleteWorkers: day.incomplete_workers,
|
||||
partialWorkers: day.partial_workers,
|
||||
errorWorkers: day.error_workers,
|
||||
overtimeWarningWorkers: day.overtime_warning_workers,
|
||||
totalHours: parseFloat(day.total_work_hours || 0),
|
||||
totalTasks: day.total_work_count,
|
||||
errorCount: day.total_error_count,
|
||||
lastUpdated: day.last_updated
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: calendarData,
|
||||
message: `${year}년 ${month}월 캘린더 데이터를 성공적으로 조회했습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 캘린더 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 캘린더 데이터 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 작업자별 상세 상태 조회
|
||||
static async getDailyWorkerDetails(req, res) {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜(date)가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`👥 일별 작업자 상세 조회: ${date}`);
|
||||
|
||||
const workerDetails = await MonthlyStatusModel.getDailyWorkerStatus(date);
|
||||
|
||||
// 프론트엔드에서 사용하기 쉽도록 데이터 변환
|
||||
const formattedData = workerDetails.map(worker => ({
|
||||
workerId: worker.worker_id,
|
||||
workerName: worker.worker_name,
|
||||
jobType: worker.job_type,
|
||||
totalHours: parseFloat(worker.total_work_hours || 0),
|
||||
actualWorkHours: parseFloat(worker.actual_work_hours || 0),
|
||||
vacationHours: parseFloat(worker.vacation_hours || 0),
|
||||
totalWorkCount: worker.total_work_count,
|
||||
regularWorkCount: worker.regular_work_count,
|
||||
errorWorkCount: worker.error_work_count,
|
||||
status: worker.work_status,
|
||||
hasVacation: worker.has_vacation,
|
||||
hasError: worker.has_error,
|
||||
hasIssues: worker.has_issues,
|
||||
lastUpdated: worker.last_updated
|
||||
}));
|
||||
|
||||
// 요약 정보 계산
|
||||
const summary = {
|
||||
totalWorkers: formattedData.length,
|
||||
totalHours: formattedData.reduce((sum, w) => sum + w.totalHours, 0),
|
||||
totalTasks: formattedData.reduce((sum, w) => sum + w.totalWorkCount, 0),
|
||||
errorCount: formattedData.reduce((sum, w) => sum + w.errorWorkCount, 0)
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workers: formattedData,
|
||||
summary
|
||||
},
|
||||
message: `${date} 작업자 상세 정보를 성공적으로 조회했습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('일별 작업자 상세 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '일별 작업자 상세 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 집계 재계산 (관리자용)
|
||||
static async recalculateMonth(req, res) {
|
||||
try {
|
||||
const { year, month } = req.body;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도(year)와 월(month)이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔄 월별 집계 재계산 시작: ${year}년 ${month}월`);
|
||||
|
||||
const result = await MonthlyStatusModel.recalculateMonth(parseInt(year), parseInt(month));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `${year}년 ${month}월 집계 재계산이 완료되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 집계 재계산 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 집계 재계산 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 집계 테이블 상태 확인 (관리자용)
|
||||
static async getStatusInfo(req, res) {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const statusInfo = await MonthlyStatusModel.getStatusInfo();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statusInfo,
|
||||
message: '집계 테이블 상태 정보를 성공적으로 조회했습니다.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('집계 테이블 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '집계 테이블 상태 확인 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonthlyStatusController;
|
||||
118
synology_deployment/api/controllers/projectController.js
Normal file
118
synology_deployment/api/controllers/projectController.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const projectModel = require('../models/projectModel');
|
||||
const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler');
|
||||
const { validateSchema, schemas } = require('../utils/validator');
|
||||
|
||||
// 1. 프로젝트 생성
|
||||
exports.createProject = asyncHandler(async (req, res) => {
|
||||
const projectData = req.body;
|
||||
|
||||
// 스키마 기반 유효성 검사
|
||||
validateSchema(projectData, schemas.createProject);
|
||||
|
||||
try {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
projectModel.create(projectData, (err, lastID) => (err ? reject(err) : resolve(lastID)));
|
||||
});
|
||||
|
||||
res.created({ project_id: id }, '프로젝트가 성공적으로 생성되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '프로젝트 생성');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllProjects = asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
projectModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
|
||||
res.list(rows, '프로젝트 목록 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '프로젝트 목록 조회');
|
||||
}
|
||||
});
|
||||
|
||||
// 2-1. 활성 프로젝트만 조회 (작업보고서용)
|
||||
exports.getActiveProjects = asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
projectModel.getActiveProjects((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
|
||||
res.list(rows, '활성 프로젝트 목록 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '활성 프로젝트 목록 조회');
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getProjectById = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(id, (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
handleNotFoundError('프로젝트', id);
|
||||
}
|
||||
|
||||
res.success(row, '프로젝트 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '프로젝트 조회');
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 수정
|
||||
exports.updateProject = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400);
|
||||
}
|
||||
|
||||
const data = { ...req.body, project_id: id };
|
||||
|
||||
try {
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
projectModel.update(data, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
|
||||
if (changes === 0) {
|
||||
handleNotFoundError('프로젝트', id);
|
||||
}
|
||||
|
||||
res.updated({ changes }, '프로젝트 정보가 성공적으로 수정되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '프로젝트 수정');
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeProject = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
projectModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
|
||||
if (changes === 0) {
|
||||
handleNotFoundError('프로젝트', id);
|
||||
}
|
||||
|
||||
res.deleted('프로젝트가 성공적으로 삭제되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '프로젝트 삭제');
|
||||
}
|
||||
});
|
||||
467
synology_deployment/api/controllers/systemController.js
Normal file
467
synology_deployment/api/controllers/systemController.js
Normal file
@@ -0,0 +1,467 @@
|
||||
// 시스템 관리 컨트롤러
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { ApiError, asyncHandler, handleDatabaseError } = require('../utils/errorHandler');
|
||||
const { validateSchema, schemas } = require('../utils/validator');
|
||||
|
||||
/**
|
||||
* 시스템 상태 확인
|
||||
*/
|
||||
exports.getSystemStatus = asyncHandler(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'
|
||||
};
|
||||
|
||||
res.health('healthy', systemStatus);
|
||||
|
||||
} catch (error) {
|
||||
handleDatabaseError(error, '시스템 상태 확인');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 데이터베이스 상태 확인
|
||||
*/
|
||||
exports.getDatabaseStatus = asyncHandler(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()
|
||||
`);
|
||||
|
||||
const dbStatus = {
|
||||
status: 'online',
|
||||
connections: parseInt(connections[0]?.Value || 0),
|
||||
max_connections: parseInt(maxConnections[0]?.Value || 0),
|
||||
size_mb: dbSize[0]?.size_mb || 0
|
||||
};
|
||||
|
||||
res.success(dbStatus, '데이터베이스 상태 조회 성공');
|
||||
|
||||
} catch (error) {
|
||||
handleDatabaseError(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 = asyncHandler(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.list(users, '사용자 목록 조회 성공');
|
||||
|
||||
} catch (error) {
|
||||
handleDatabaseError(error, '사용자 목록 조회');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 생성
|
||||
*/
|
||||
exports.createUser = asyncHandler(async (req, res) => {
|
||||
const { username, password, name, email, role, access_level, worker_id } = req.body;
|
||||
|
||||
// 스키마 기반 유효성 검사
|
||||
validateSchema(req.body, schemas.createUser);
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자명 중복 확인
|
||||
const [existing] = await db.query('SELECT user_id FROM users WHERE username = ?', [username]);
|
||||
if (existing.length > 0) {
|
||||
throw new ApiError('이미 존재하는 사용자명입니다.', 409);
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await db.query('SELECT user_id FROM users WHERE email = ?', [email]);
|
||||
if (existingEmail.length > 0) {
|
||||
throw new ApiError('이미 사용 중인 이메일입니다.', 409);
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
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.created({ user_id: result.insertId }, '사용자가 성공적으로 생성되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
handleDatabaseError(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: '비밀번호 재설정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
76
synology_deployment/api/controllers/toolsController.js
Normal file
76
synology_deployment/api/controllers/toolsController.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const Tools = require('../models/toolsModel');
|
||||
|
||||
// 1. 전체 도구 조회
|
||||
exports.getAll = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
Tools.getAllTools((err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 단일 도구 조회
|
||||
exports.getById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
Tools.getToolById(id, (err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Tool not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 도구 생성
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const insertId = await new Promise((resolve, reject) => {
|
||||
Tools.createTool(req.body, (err, resultId) => {
|
||||
if (err) return reject(err);
|
||||
resolve(resultId);
|
||||
});
|
||||
});
|
||||
res.status(201).json({ success: true, id: insertId });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 도구 수정
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changedRows = await new Promise((resolve, reject) => {
|
||||
Tools.updateTool(id, req.body, (err, affectedRows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changedRows === 0) return res.status(404).json({ error: 'Tool not found or no change' });
|
||||
res.json({ success: true, changes: changedRows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 도구 삭제
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const deletedRows = await new Promise((resolve, reject) => {
|
||||
Tools.deleteTool(id, (err, affectedRows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (deletedRows === 0) return res.status(404).json({ error: 'Tool not found' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
26
synology_deployment/api/controllers/uploadController.js
Normal file
26
synology_deployment/api/controllers/uploadController.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const uploadModel = require('../models/uploadModel');
|
||||
|
||||
// 1. 문서 업로드
|
||||
exports.createUpload = async (req, res) => {
|
||||
try {
|
||||
const doc = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
uploadModel.create(doc, (err, insertId) => (err ? reject(err) : resolve(insertId)));
|
||||
});
|
||||
res.status(201).json({ success: true, id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 업로드 문서 조회
|
||||
exports.getUploads = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
uploadModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
247
synology_deployment/api/controllers/userController.js
Normal file
247
synology_deployment/api/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
|
||||
};
|
||||
523
synology_deployment/api/controllers/workAnalysisController.js
Normal file
523
synology_deployment/api/controllers/workAnalysisController.js
Normal file
@@ -0,0 +1,523 @@
|
||||
// controllers/workAnalysisController.js
|
||||
|
||||
const WorkAnalysis = require('../models/WorkAnalysis');
|
||||
const { getDb } = require('../dbPool'); // 기존 프로젝트의 DB 연결 방식 사용
|
||||
|
||||
class WorkAnalysisController {
|
||||
constructor() {
|
||||
// 메서드 바인딩
|
||||
this.getStats = this.getStats.bind(this);
|
||||
this.getDailyTrend = this.getDailyTrend.bind(this);
|
||||
this.getWorkerStats = this.getWorkerStats.bind(this);
|
||||
this.getProjectStats = this.getProjectStats.bind(this);
|
||||
this.getWorkTypeStats = this.getWorkTypeStats.bind(this);
|
||||
this.getRecentWork = this.getRecentWork.bind(this);
|
||||
this.getWeekdayPattern = this.getWeekdayPattern.bind(this);
|
||||
this.getErrorAnalysis = this.getErrorAnalysis.bind(this);
|
||||
this.getMonthlyComparison = this.getMonthlyComparison.bind(this);
|
||||
this.getWorkerSpecialization = this.getWorkerSpecialization.bind(this);
|
||||
this.getProjectWorkTypeAnalysis = this.getProjectWorkTypeAnalysis.bind(this);
|
||||
}
|
||||
|
||||
// 날짜 유효성 검사
|
||||
validateDateRange(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
throw new Error('시작일과 종료일을 입력해주세요.');
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
throw new Error('올바른 날짜 형식을 입력해주세요. (YYYY-MM-DD)');
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
throw new Error('시작일이 종료일보다 늦을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 너무 긴 기간 방지 (1년 제한)
|
||||
const diffTime = Math.abs(end - start);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
if (diffDays > 365) {
|
||||
throw new Error('조회 기간은 1년을 초과할 수 없습니다.');
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
// 기본 통계 조회
|
||||
async getStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const stats = await workAnalysis.getBasicStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: stats,
|
||||
message: '기본 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('기본 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 일별 작업시간 추이
|
||||
async getDailyTrend(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const trendData = await workAnalysis.getDailyTrend(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: trendData,
|
||||
message: '일별 추이 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('일별 추이 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 통계
|
||||
async getWorkerStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workerStats = await workAnalysis.getWorkerStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: workerStats,
|
||||
message: '작업자별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 통계
|
||||
async getProjectStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const projectStats = await workAnalysis.getProjectStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: projectStats,
|
||||
message: '프로젝트별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업유형별 통계
|
||||
async getWorkTypeStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workTypeStats = await workAnalysis.getWorkTypeStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: workTypeStats,
|
||||
message: '작업유형별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업유형별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 작업 현황
|
||||
async getRecentWork(req, res) {
|
||||
try {
|
||||
const { start, end, limit = 10 } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
// limit 유효성 검사 (최대 5000까지 허용)
|
||||
const limitNum = parseInt(limit);
|
||||
if (isNaN(limitNum) || limitNum < 1 || limitNum > 5000) {
|
||||
throw new Error('limit은 1~5000 사이의 숫자여야 합니다.');
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const recentWork = await workAnalysis.getRecentWork(start, end, limitNum);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: recentWork,
|
||||
message: '최근 작업 현황 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('최근 작업 현황 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 요일별 패턴 분석
|
||||
async getWeekdayPattern(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const weekdayPattern = await workAnalysis.getWeekdayPattern(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: weekdayPattern,
|
||||
message: '요일별 패턴 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('요일별 패턴 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 분석
|
||||
async getErrorAnalysis(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const errorAnalysis = await workAnalysis.getErrorAnalysis(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: errorAnalysis,
|
||||
message: '에러 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('에러 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 비교 분석
|
||||
async getMonthlyComparison(req, res) {
|
||||
try {
|
||||
const { year = new Date().getFullYear() } = req.query;
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2050) {
|
||||
throw new Error('올바른 연도를 입력해주세요. (2000-2050)');
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const monthlyData = await workAnalysis.getMonthlyComparison(yearNum);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: monthlyData,
|
||||
message: '월별 비교 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 비교 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 전문분야 분석
|
||||
async getWorkerSpecialization(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const specializationData = await workAnalysis.getWorkerSpecialization(start, end);
|
||||
|
||||
// 작업자별로 그룹화하여 정리
|
||||
const groupedData = specializationData.reduce((acc, item) => {
|
||||
if (!acc[item.worker_id]) {
|
||||
acc[item.worker_id] = [];
|
||||
}
|
||||
acc[item.worker_id].push({
|
||||
work_type_id: item.work_type_id,
|
||||
project_id: item.project_id,
|
||||
totalHours: item.totalHours,
|
||||
totalReports: item.totalReports,
|
||||
percentage: item.percentage
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: groupedData,
|
||||
message: '작업자별 전문분야 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자별 전문분야 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대시보드용 종합 데이터
|
||||
async getDashboardData(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
|
||||
// 병렬로 여러 데이터 조회
|
||||
const [
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
] = await Promise.all([
|
||||
workAnalysis.getBasicStats(start, end),
|
||||
workAnalysis.getDailyTrend(start, end),
|
||||
workAnalysis.getWorkerStats(start, end),
|
||||
workAnalysis.getProjectStats(start, end),
|
||||
workAnalysis.getWorkTypeStats(start, end),
|
||||
workAnalysis.getRecentWork(start, end, 10)
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
},
|
||||
message: '대시보드 데이터 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('대시보드 데이터 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
|
||||
async getProjectWorkTypeAnalysis(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 데이터 존재 여부 확인
|
||||
const testQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date,
|
||||
SUM(work_hours) as total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const testResults = await db.query(testQuery, [start, end]);
|
||||
console.log('📊 데이터 확인:', testResults[0]);
|
||||
|
||||
// 먼저 간단한 테스트 쿼리로 데이터 확인
|
||||
const simpleQuery = `
|
||||
SELECT COUNT(*) as count, MIN(report_date) as min_date, MAX(report_date) as max_date
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const simpleResult = await db.query(simpleQuery, [start, end]);
|
||||
console.log('📊 기간 내 데이터 확인:', simpleResult[0][0]);
|
||||
|
||||
// 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인)
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(p.project_id, dwr.project_id) as project_id,
|
||||
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
|
||||
COALESCE(p.job_no, 'N/A') as job_no,
|
||||
dwr.work_type_id,
|
||||
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
|
||||
|
||||
-- 총 시간
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
|
||||
-- 정규 시간 (work_status_id = 1)
|
||||
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
|
||||
|
||||
-- 에러 시간 (work_status_id = 2)
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
|
||||
|
||||
-- 작업 건수
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
|
||||
|
||||
-- 에러율 계산
|
||||
ROUND(
|
||||
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
|
||||
SUM(dwr.work_hours)) * 100, 2
|
||||
) as error_rate_percent
|
||||
|
||||
FROM daily_work_reports dwr
|
||||
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
|
||||
ORDER BY p.project_name, wt.name
|
||||
`;
|
||||
|
||||
const results = await db.query(query, [start, end]);
|
||||
console.log('📊 쿼리 결과 개수:', results[0].length);
|
||||
console.log('📊 첫 번째 결과:', results[0][0]);
|
||||
console.log('📊 모든 결과:', JSON.stringify(results[0], null, 2));
|
||||
|
||||
// 데이터를 프로젝트별로 그룹화
|
||||
const groupedData = {};
|
||||
|
||||
results[0].forEach(row => {
|
||||
const projectKey = `${row.project_id}_${row.project_name}`;
|
||||
|
||||
if (!groupedData[projectKey]) {
|
||||
groupedData[projectKey] = {
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name,
|
||||
job_no: row.job_no,
|
||||
total_project_hours: 0,
|
||||
total_regular_hours: 0,
|
||||
total_error_hours: 0,
|
||||
work_types: []
|
||||
};
|
||||
}
|
||||
|
||||
// 프로젝트 총계 누적
|
||||
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
|
||||
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
|
||||
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
|
||||
|
||||
// 작업 유형별 데이터 추가
|
||||
groupedData[projectKey].work_types.push({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name,
|
||||
total_hours: parseFloat(row.total_hours),
|
||||
regular_hours: parseFloat(row.regular_hours),
|
||||
error_hours: parseFloat(row.error_hours),
|
||||
total_reports: row.total_reports,
|
||||
regular_reports: row.regular_reports,
|
||||
error_reports: row.error_reports,
|
||||
error_rate_percent: parseFloat(row.error_rate_percent) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// 프로젝트별 에러율 계산
|
||||
Object.values(groupedData).forEach(project => {
|
||||
project.project_error_rate = project.total_project_hours > 0
|
||||
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
// 전체 요약 통계
|
||||
const totalStats = {
|
||||
total_projects: Object.keys(groupedData).length,
|
||||
total_work_types: new Set(results[0].map(r => r.work_type_id)).size,
|
||||
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
|
||||
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
|
||||
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
|
||||
};
|
||||
|
||||
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
|
||||
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: totalStats,
|
||||
projects: Object.values(groupedData),
|
||||
period: { start, end }
|
||||
},
|
||||
message: '프로젝트별-작업별 시간 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별-작업별 시간 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WorkAnalysisController();
|
||||
@@ -0,0 +1,381 @@
|
||||
// controllers/workReportAnalysisController.js - 데일리 워크 레포트 분석 전용 컨트롤러
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 📋 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
|
||||
*/
|
||||
const getAnalysisFilters = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 프로젝트 목록
|
||||
const [projects] = await db.query(`
|
||||
SELECT DISTINCT p.project_id, p.project_name
|
||||
FROM projects p
|
||||
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
|
||||
ORDER BY p.project_name
|
||||
`);
|
||||
|
||||
// 작업자 목록
|
||||
const [workers] = await db.query(`
|
||||
SELECT DISTINCT w.worker_id, w.worker_name
|
||||
FROM workers w
|
||||
INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id
|
||||
ORDER BY w.worker_name
|
||||
`);
|
||||
|
||||
// 작업 유형 목록
|
||||
const [workTypes] = await db.query(`
|
||||
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
|
||||
FROM work_types wt
|
||||
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
|
||||
ORDER BY wt.name
|
||||
`);
|
||||
|
||||
// 날짜 범위 (최초/최신 데이터)
|
||||
const [dateRange] = await db.query(`
|
||||
SELECT
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date
|
||||
FROM daily_work_reports
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
projects,
|
||||
workers,
|
||||
workTypes,
|
||||
dateRange: dateRange[0]
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('필터 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '필터 데이터 조회 중 오류가 발생했습니다.',
|
||||
detail: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기간별 작업 분석 데이터 조회
|
||||
*/
|
||||
const getAnalyticsByPeriod = async (req, res) => {
|
||||
try {
|
||||
const { start_date, end_date, project_id, worker_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2025-08-01&end_date=2025-08-31'
|
||||
});
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 기본 조건
|
||||
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
||||
let queryParams = [start_date, end_date];
|
||||
|
||||
// 프로젝트 필터
|
||||
if (project_id) {
|
||||
whereConditions.push('dwr.project_id = ?');
|
||||
queryParams.push(project_id);
|
||||
}
|
||||
|
||||
// 작업자 필터
|
||||
if (worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(worker_id);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 1. 전체 요약 통계 (에러 분석 포함)
|
||||
const overallSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.worker_id) as unique_workers,
|
||||
COUNT(DISTINCT dwr.project_id) as unique_projects,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(DISTINCT dwr.created_by) as contributors,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_entries,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [overallStats] = await db.query(overallSql, queryParams);
|
||||
|
||||
// 2. 일별 통계
|
||||
const dailyStatsSql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
SUM(dwr.work_hours) as daily_hours,
|
||||
COUNT(*) as daily_entries,
|
||||
COUNT(DISTINCT dwr.worker_id) as daily_workers
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`;
|
||||
|
||||
const [dailyStats] = await db.query(dailyStatsSql, queryParams);
|
||||
|
||||
// 2.5. 일별 에러 발생 통계
|
||||
const dailyErrorStatsSql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
|
||||
COUNT(*) as daily_total,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`;
|
||||
|
||||
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
|
||||
|
||||
// 3. 에러 유형별 분석 (간단한 방식으로 수정)
|
||||
const errorAnalysisSql = `
|
||||
SELECT
|
||||
et.id as error_type_id,
|
||||
et.name as error_type_name,
|
||||
COUNT(*) as error_count,
|
||||
SUM(dwr.work_hours) as error_hours,
|
||||
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
|
||||
GROUP BY et.id, et.name
|
||||
ORDER BY error_count DESC
|
||||
`;
|
||||
|
||||
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
|
||||
|
||||
// 4. 작업 유형별 분석
|
||||
const workTypeAnalysisSql = `
|
||||
SELECT
|
||||
wt.id as work_type_id,
|
||||
wt.name as work_type_name,
|
||||
COUNT(*) as work_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY wt.id, wt.name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
|
||||
|
||||
// 5. 작업자별 성과 분석
|
||||
const workerAnalysisSql = `
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.worker_id, w.worker_name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
|
||||
|
||||
// 6. 프로젝트별 분석
|
||||
const projectAnalysisSql = `
|
||||
SELECT
|
||||
p.project_id,
|
||||
p.project_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.worker_id) as workers_count,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY p.project_id, p.project_name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: overallStats[0],
|
||||
dailyStats,
|
||||
dailyErrorStats,
|
||||
errorAnalysis,
|
||||
workTypeAnalysis,
|
||||
workerAnalysis,
|
||||
projectAnalysis,
|
||||
period: { start_date, end_date },
|
||||
filters: { project_id, worker_id }
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('기간별 분석 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '기간별 분석 데이터 조회 중 오류가 발생했습니다.',
|
||||
detail: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 프로젝트별 상세 분석
|
||||
*/
|
||||
const getProjectAnalysis = async (req, res) => {
|
||||
try {
|
||||
const { start_date, end_date, project_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'start_date와 end_date가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
||||
let queryParams = [start_date, end_date];
|
||||
|
||||
if (project_id) {
|
||||
whereConditions.push('dwr.project_id = ?');
|
||||
queryParams.push(project_id);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 프로젝트별 통계
|
||||
const projectStatsSql = `
|
||||
SELECT
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(*) as total_entries,
|
||||
COUNT(DISTINCT dwr.worker_id) as workers_count,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.project_id
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [projectStats] = await db.query(projectStatsSql, queryParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
projectStats,
|
||||
period: { start_date, end_date }
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별 분석 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '프로젝트별 분석 데이터 조회 중 오류가 발생했습니다.',
|
||||
detail: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 👤 작업자별 상세 분석
|
||||
*/
|
||||
const getWorkerAnalysis = async (req, res) => {
|
||||
try {
|
||||
const { start_date, end_date, worker_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'start_date와 end_date가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
||||
let queryParams = [start_date, end_date];
|
||||
|
||||
if (worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(worker_id);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 작업자별 통계
|
||||
const workerStatsSql = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(*) as total_entries,
|
||||
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.worker_id
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [workerStats] = await db.query(workerStatsSql, queryParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workerStats,
|
||||
period: { start_date, end_date }
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자별 분석 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '작업자별 분석 데이터 조회 중 오류가 발생했습니다.',
|
||||
detail: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAnalysisFilters,
|
||||
getAnalyticsByPeriod,
|
||||
getProjectAnalysis,
|
||||
getWorkerAnalysis
|
||||
};
|
||||
134
synology_deployment/api/controllers/workReportController.js
Normal file
134
synology_deployment/api/controllers/workReportController.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// controllers/workReportController.js
|
||||
const workReportModel = require('../models/workReportModel');
|
||||
|
||||
// 1. CREATE: 단일 또는 다중 보고서 등록
|
||||
exports.createWorkReport = async (req, res) => {
|
||||
try {
|
||||
const reports = Array.isArray(req.body) ? req.body : [req.body];
|
||||
const workReport_ids = [];
|
||||
|
||||
for (const report of reports) {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
workReportModel.create(report, (err, insertId) => {
|
||||
if (err) reject(err);
|
||||
else resolve(insertId);
|
||||
});
|
||||
});
|
||||
workReport_ids.push(id);
|
||||
}
|
||||
|
||||
res.json({ success: true, workReport_ids });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. READ BY DATE
|
||||
exports.getWorkReportsByDate = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.params;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getAllByDate(date, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. READ BY RANGE
|
||||
exports.getWorkReportsInRange = async (req, res) => {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getByRange(start, end, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. READ ONE
|
||||
exports.getWorkReportById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
workReportModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'WorkReport not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. UPDATE
|
||||
exports.updateWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workReportModel.update(id, req.body, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 6. DELETE
|
||||
exports.removeWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workReportModel.remove(id, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'WorkReport not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 7. SUMMARY (월간)
|
||||
exports.getSummary = async (req, res) => {
|
||||
try {
|
||||
const { year, month } = req.query;
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({ error: '연도와 월이 필요합니다 (year, month)' });
|
||||
}
|
||||
|
||||
const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`;
|
||||
const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`;
|
||||
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getByRange(start, end, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!rows || rows.length === 0) {
|
||||
return res.status(404).json({ error: 'WorkReport not found' });
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
148
synology_deployment/api/controllers/workerController.js
Normal file
148
synology_deployment/api/controllers/workerController.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// controllers/workerController.js
|
||||
const workerModel = require('../models/workerModel');
|
||||
const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler');
|
||||
const { validateSchema, schemas } = require('../utils/validator');
|
||||
const cache = require('../utils/cache');
|
||||
const { optimizedQueries } = require('../utils/queryOptimizer');
|
||||
|
||||
// 1. 작업자 생성
|
||||
exports.createWorker = asyncHandler(async (req, res) => {
|
||||
const workerData = req.body;
|
||||
|
||||
// 스키마 기반 유효성 검사
|
||||
validateSchema(workerData, schemas.createWorker);
|
||||
|
||||
try {
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
workerModel.create(workerData, (err, id) => {
|
||||
if (err) reject(err);
|
||||
else resolve(id);
|
||||
});
|
||||
});
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
await cache.invalidateCache.worker();
|
||||
|
||||
res.created({ worker_id: lastID }, '작업자가 성공적으로 생성되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업자 생성');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 전체 작업자 조회 (캐싱 및 페이지네이션 적용)
|
||||
exports.getAllWorkers = asyncHandler(async (req, res) => {
|
||||
const { page = 1, limit = 10, search = '' } = req.query;
|
||||
|
||||
// 캐시 키 생성
|
||||
const cacheKey = cache.createKey('workers', 'list', page, limit, search);
|
||||
|
||||
try {
|
||||
// 캐시에서 조회
|
||||
const cachedData = await cache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
console.log(`🎯 캐시 히트: ${cacheKey}`);
|
||||
return res.paginated(cachedData.data, cachedData.pagination.totalCount, page, limit, '작업자 목록 조회 성공 (캐시)');
|
||||
}
|
||||
|
||||
// 최적화된 쿼리 사용
|
||||
const result = await optimizedQueries.getWorkersPaged(page, limit, search);
|
||||
|
||||
// 캐시에 저장 (5분)
|
||||
await cache.set(cacheKey, result, cache.TTL.MEDIUM);
|
||||
console.log(`💾 캐시 저장: ${cacheKey}`);
|
||||
|
||||
res.paginated(result.data, result.pagination.totalCount, page, limit, '작업자 목록 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업자 목록 조회');
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 단일 작업자 조회
|
||||
exports.getWorkerById = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 작업자 ID입니다.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
workerModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
handleNotFoundError('작업자', id);
|
||||
}
|
||||
|
||||
res.success(row, '작업자 조회 성공');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업자 조회');
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 작업자 수정
|
||||
exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 작업자 ID입니다.', 400);
|
||||
}
|
||||
|
||||
const workerData = { ...req.body, worker_id: id };
|
||||
|
||||
try {
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workerModel.update(workerData, (err, affected) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affected);
|
||||
});
|
||||
});
|
||||
|
||||
if (changes === 0) {
|
||||
handleNotFoundError('작업자', id);
|
||||
}
|
||||
|
||||
res.updated({ changes }, '작업자 정보가 성공적으로 수정되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업자 수정');
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 작업자 삭제
|
||||
exports.removeWorker = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ApiError('유효하지 않은 작업자 ID입니다.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workerModel.remove(id, (err, affected) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affected);
|
||||
});
|
||||
});
|
||||
|
||||
if (changes === 0) {
|
||||
handleNotFoundError('작업자', id);
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
console.log('🗑️ 작업자 삭제 후 캐시 무효화 시작...');
|
||||
await cache.invalidateCache.worker();
|
||||
|
||||
// 추가로 전체 작업자 캐시도 강제 무효화
|
||||
await cache.delPattern('workers:*');
|
||||
await cache.flush(); // 전체 캐시 초기화 (임시)
|
||||
|
||||
console.log('✅ 작업자 삭제 후 캐시 무효화 완료');
|
||||
|
||||
res.deleted('작업자가 성공적으로 삭제되었습니다.');
|
||||
} catch (err) {
|
||||
handleDatabaseError(err, '작업자 삭제');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user