feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 프로젝트 분석 컨트롤러
|
||||
*
|
||||
* 기간별 프로젝트 분석 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const analysisService = require('../services/analysisService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 프로젝트 분석 데이터 조회
|
||||
*/
|
||||
const getAnalysisData = asyncHandler(async (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
const data = await analysisService.getAnalysisService(startDate, endDate);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '분석 데이터 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAnalysisData
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 근태 관리 컨트롤러
|
||||
*
|
||||
* 근태 기록 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const attendanceService = require('../services/attendanceService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 일일 근태 현황 조회 (대시보드용)
|
||||
*/
|
||||
const getDailyAttendanceStatus = asyncHandler(async (req, res) => {
|
||||
const { date } = req.query;
|
||||
const data = await attendanceService.getDailyAttendanceStatusService(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '근태 현황을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 일일 근태 기록 조회
|
||||
*/
|
||||
const getDailyAttendanceRecords = asyncHandler(async (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const data = await attendanceService.getDailyAttendanceRecordsService(date, worker_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '근태 기록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 기간별 근태 기록 조회 (월별 조회용)
|
||||
*/
|
||||
const getAttendanceRecordsByRange = asyncHandler(async (req, res) => {
|
||||
const { start_date, end_date, worker_id } = req.query;
|
||||
const data = await attendanceService.getAttendanceRecordsByRangeService(start_date, end_date, worker_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '근태 기록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 근태 기록 생성/업데이트
|
||||
*/
|
||||
const upsertAttendanceRecord = asyncHandler(async (req, res) => {
|
||||
const recordData = {
|
||||
...req.body,
|
||||
created_by: req.user?.user_id || req.user?.id
|
||||
};
|
||||
|
||||
const result = await attendanceService.upsertAttendanceRecordService(recordData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '근태 기록이 성공적으로 저장되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 휴가 처리
|
||||
*/
|
||||
const processVacation = asyncHandler(async (req, res) => {
|
||||
const vacationData = {
|
||||
record_date: req.body.date,
|
||||
worker_id: req.body.worker_id,
|
||||
vacation_type_id: req.body.vacation_type,
|
||||
created_by: req.user?.user_id || req.user?.id
|
||||
};
|
||||
|
||||
const result = await attendanceService.processVacationService(vacationData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '휴가 처리가 성공적으로 완료되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 초과근무 승인
|
||||
*/
|
||||
const approveOvertime = asyncHandler(async (req, res) => {
|
||||
const overtimeData = {
|
||||
record_date: req.body.date,
|
||||
worker_id: req.body.worker_id,
|
||||
overtime_approved: true,
|
||||
approved_by: req.user?.user_id || req.user?.id
|
||||
};
|
||||
|
||||
const result = await attendanceService.approveOvertimeService(overtimeData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '초과근무가 성공적으로 승인되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 근로 유형 목록 조회
|
||||
*/
|
||||
const getAttendanceTypes = asyncHandler(async (req, res) => {
|
||||
const data = await attendanceService.getAttendanceTypesService();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '근로 유형 목록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 휴가 유형 목록 조회
|
||||
*/
|
||||
const getVacationTypes = asyncHandler(async (req, res) => {
|
||||
const data = await attendanceService.getVacationTypesService();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '휴가 유형 목록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자 휴가 잔여 조회
|
||||
*/
|
||||
const getWorkerVacationBalance = asyncHandler(async (req, res) => {
|
||||
const { worker_id } = req.params;
|
||||
const data = await attendanceService.getWorkerVacationBalanceService(parseInt(worker_id));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '휴가 잔여 정보를 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 월별 근태 통계
|
||||
*/
|
||||
const getMonthlyAttendanceStats = asyncHandler(async (req, res) => {
|
||||
const { year, month, worker_id } = req.query;
|
||||
const data = await attendanceService.getMonthlyAttendanceStatsService(
|
||||
parseInt(year),
|
||||
parseInt(month),
|
||||
worker_id ? parseInt(worker_id) : null
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '월별 근태 통계를 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 출근 체크 목록 조회 (아침용, 휴가 정보 포함)
|
||||
*/
|
||||
const getCheckinList = asyncHandler(async (req, res) => {
|
||||
const { date } = req.query;
|
||||
const data = await attendanceService.getCheckinListService(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '출근 체크 목록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 출근 체크 저장 (일괄 처리)
|
||||
*/
|
||||
const saveCheckins = asyncHandler(async (req, res) => {
|
||||
const { date, checkins } = req.body; // checkins: [{worker_id, is_present}, ...]
|
||||
const result = await attendanceService.saveCheckinsService(date, checkins);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '출근 체크가 성공적으로 저장되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getDailyAttendanceStatus,
|
||||
getDailyAttendanceRecords,
|
||||
getAttendanceRecordsByRange,
|
||||
upsertAttendanceRecord,
|
||||
processVacation,
|
||||
approveOvertime,
|
||||
getAttendanceTypes,
|
||||
getVacationTypes,
|
||||
getWorkerVacationBalance,
|
||||
getMonthlyAttendanceStats,
|
||||
getCheckinList,
|
||||
saveCheckins
|
||||
};
|
||||
161
deploy/tkfb-package/api.hyungi.net/controllers/authController.js
Normal file
161
deploy/tkfb-package/api.hyungi.net/controllers/authController.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authService = require('../services/auth.service');
|
||||
const { asyncHandler } = require('../utils/errorHandler');
|
||||
const { AuthenticationError, ValidationError } = require('../utils/errors');
|
||||
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 ValidationError('사용자명과 비밀번호를 입력해주세요.');
|
||||
}
|
||||
|
||||
const result = await authService.loginService(username, password, ipAddress, userAgent);
|
||||
|
||||
if (!result.success) {
|
||||
throw new AuthenticationError(result.error);
|
||||
}
|
||||
|
||||
// 로그인 성공 후, 메인 대시보드로 리다이렉트
|
||||
const user = result.data.user;
|
||||
const redirectUrl = '/pages/dashboard.html'; // 메인 대시보드로 리다이렉트
|
||||
|
||||
// 새로운 응답 포맷터 사용
|
||||
res.auth(user, result.data.token, redirectUrl, '로그인 성공');
|
||||
});
|
||||
|
||||
// ✅ 사용자 등록 기능 추가
|
||||
const 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 사용자 삭제 기능 추가
|
||||
const 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 사용자 목록 조회
|
||||
const 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,
|
||||
register,
|
||||
deleteUser,
|
||||
getAllUsers
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 일일 이슈 보고서 관리 컨트롤러
|
||||
*
|
||||
* 일일 이슈 보고서 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const dailyIssueReportService = require('../services/dailyIssueReportService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 일일 이슈 보고서 생성
|
||||
*/
|
||||
const createDailyIssueReport = asyncHandler(async (req, res) => {
|
||||
// 프론트엔드에서 worker_ids 또는 worker_id로 보낼 수 있음
|
||||
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,
|
||||
data: result,
|
||||
message: result.message
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 날짜별 이슈 조회
|
||||
*/
|
||||
const getDailyIssuesByDate = asyncHandler(async (req, res) => {
|
||||
const { date } = req.query;
|
||||
const issues = await dailyIssueReportService.getDailyIssuesByDateService(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: issues,
|
||||
message: '이슈 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 이슈 보고서 삭제
|
||||
*/
|
||||
const removeDailyIssue = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const result = await dailyIssueReportService.removeDailyIssueService(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: result.message
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createDailyIssueReport,
|
||||
getDailyIssuesByDate,
|
||||
removeDailyIssue
|
||||
};
|
||||
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* 일일 작업 보고서 컨트롤러
|
||||
*
|
||||
* 작업 보고서 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
const dailyWorkReportService = require('../services/dailyWorkReportService');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (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 || '알 수 없는 사용자'
|
||||
};
|
||||
|
||||
const result = await dailyWorkReportService.createDailyWorkReportService(reportData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '작업보고서가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
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 사용)
|
||||
* 권한: 그룹장(group_leader), 시스템(system), 관리자(admin)만 가능
|
||||
*/
|
||||
const removeDailyWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id: reportId } = req.params;
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
access_level: req.user?.access_level || req.user?.role,
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(userInfo.access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await dailyWorkReportService.removeDailyWorkReportService(reportId, userInfo);
|
||||
|
||||
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;
|
||||
const access_level = req.user?.access_level || req.user?.role;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
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({
|
||||
success: false,
|
||||
error: {
|
||||
message: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
code: 'DATABASE_ERROR'
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
success: true,
|
||||
data: data,
|
||||
message: '작업 유형 조회 성공'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* TBM 배정 기반 작업보고서 생성
|
||||
*/
|
||||
const createFromTbm = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
tbm_assignment_id,
|
||||
tbm_session_id,
|
||||
worker_id,
|
||||
project_id,
|
||||
work_type_id,
|
||||
report_date,
|
||||
start_time,
|
||||
end_time,
|
||||
total_hours,
|
||||
error_hours,
|
||||
error_type_id,
|
||||
work_status_id
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!tbm_assignment_id || !tbm_session_id || !worker_id || !report_date || !total_hours) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다. (assignment_id, session_id, worker_id, report_date, total_hours)'
|
||||
});
|
||||
}
|
||||
|
||||
// regular_hours 계산
|
||||
const regular_hours = total_hours - (error_hours || 0);
|
||||
|
||||
const reportData = {
|
||||
tbm_assignment_id,
|
||||
tbm_session_id,
|
||||
worker_id,
|
||||
project_id,
|
||||
work_type_id,
|
||||
report_date,
|
||||
start_time,
|
||||
end_time,
|
||||
total_hours,
|
||||
error_hours: error_hours || 0,
|
||||
regular_hours,
|
||||
work_status_id: work_status_id || (error_hours > 0 ? 2 : 1), // error_hours가 있으면 상태 2 (부적합)
|
||||
error_type_id,
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
const result = await dailyWorkReportModel.createFromTbmAssignment(reportData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '작업보고서가 생성되었습니다.',
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('TBM 작업보고서 생성 오류:', err);
|
||||
console.error('Error stack:', err.stack);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 작업보고서 생성 중 오류가 발생했습니다.',
|
||||
error: err.message,
|
||||
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (리팩토링된 함수 위주로 재구성)
|
||||
module.exports = {
|
||||
// 📝 V2 핵심 CRUD 함수
|
||||
createDailyWorkReport,
|
||||
getDailyWorkReports,
|
||||
updateWorkReport,
|
||||
removeDailyWorkReport,
|
||||
createFromTbm,
|
||||
|
||||
// 📊 V2 통계 및 요약 함수
|
||||
getWorkReportStats,
|
||||
getDailySummary,
|
||||
|
||||
// 🔽 아직 리팩토링되지 않은 레거시 함수들
|
||||
getAccumulatedReports,
|
||||
getContributorsSummary,
|
||||
getMyAccumulatedData,
|
||||
removeMyEntry,
|
||||
getDailyWorkReportsByDate,
|
||||
searchWorkReports,
|
||||
getMonthlySummary,
|
||||
removeDailyWorkReportByDateAndWorker,
|
||||
getWorkTypes,
|
||||
getWorkStatusTypes,
|
||||
getErrorTypes,
|
||||
|
||||
// 🔽 마스터 데이터 CRUD
|
||||
createWorkType,
|
||||
updateWorkType,
|
||||
deleteWorkType,
|
||||
createWorkStatus,
|
||||
updateWorkStatus,
|
||||
deleteWorkStatus,
|
||||
createErrorType,
|
||||
updateErrorType,
|
||||
deleteErrorType
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
// controllers/departmentController.js
|
||||
const departmentModel = require('../models/departmentModel');
|
||||
|
||||
const departmentController = {
|
||||
// 모든 부서 조회
|
||||
async getAll(req, res) {
|
||||
try {
|
||||
const { active_only } = req.query;
|
||||
const departments = active_only === 'true'
|
||||
? await departmentModel.getActive()
|
||||
: await departmentModel.getAll();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: departments
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '부서 목록을 불러오는데 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 부서 상세 조회
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const department = await departmentModel.getById(id);
|
||||
|
||||
if (!department) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '부서를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: department
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '부서 정보를 불러오는데 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 부서 생성
|
||||
async create(req, res) {
|
||||
try {
|
||||
const { department_name, parent_id, description, is_active, display_order } = req.body;
|
||||
|
||||
if (!department_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '부서명은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const departmentId = await departmentModel.create({
|
||||
department_name,
|
||||
parent_id,
|
||||
description,
|
||||
is_active,
|
||||
display_order
|
||||
});
|
||||
|
||||
const newDepartment = await departmentModel.getById(departmentId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '부서가 생성되었습니다.',
|
||||
data: newDepartment
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '부서 생성에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 부서 수정
|
||||
async update(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { department_name, parent_id, description, is_active, display_order } = req.body;
|
||||
|
||||
if (!department_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '부서명은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 자기 자신을 상위 부서로 지정하는 것 방지
|
||||
if (parent_id && parseInt(parent_id) === parseInt(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '자기 자신을 상위 부서로 지정할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await departmentModel.update(id, {
|
||||
department_name,
|
||||
parent_id,
|
||||
description,
|
||||
is_active,
|
||||
display_order
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '부서를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDepartment = await departmentModel.getById(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '부서 정보가 수정되었습니다.',
|
||||
data: updatedDepartment
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '부서 수정에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 부서 삭제
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await departmentModel.delete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '부서가 삭제되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 삭제 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message || '부서 삭제에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 부서별 작업자 조회
|
||||
async getWorkers(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const workers = await departmentModel.getWorkersByDepartment(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: workers
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('부서 작업자 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '작업자 목록을 불러오는데 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 작업자 부서 이동
|
||||
async moveWorker(req, res) {
|
||||
try {
|
||||
const { workerId, departmentId } = req.body;
|
||||
|
||||
if (!workerId || !departmentId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '작업자 ID와 부서 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
await departmentModel.moveWorker(workerId, departmentId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업자 부서가 변경되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('작업자 부서 이동 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '작업자 부서 변경에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 여러 작업자 부서 일괄 이동
|
||||
async moveWorkers(req, res) {
|
||||
try {
|
||||
const { workerIds, departmentId } = req.body;
|
||||
|
||||
if (!workerIds || !Array.isArray(workerIds) || workerIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '이동할 작업자를 선택하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!departmentId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '대상 부서를 선택하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
const count = await departmentModel.moveWorkers(workerIds, departmentId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${count}명의 작업자 부서가 변경되었습니다.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('작업자 일괄 이동 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '작업자 부서 변경에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = departmentController;
|
||||
@@ -0,0 +1,945 @@
|
||||
// controllers/equipmentController.js
|
||||
const EquipmentModel = require('../models/equipmentModel');
|
||||
const imageUploadService = require('../services/imageUploadService');
|
||||
|
||||
const EquipmentController = {
|
||||
// CREATE - 설비 생성
|
||||
createEquipment: async (req, res) => {
|
||||
try {
|
||||
const equipmentData = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!equipmentData.equipment_code || !equipmentData.equipment_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '설비 코드와 설비명은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 설비 코드 중복 확인
|
||||
EquipmentModel.checkDuplicateCode(equipmentData.equipment_code, null, (error, isDuplicate) => {
|
||||
if (error) {
|
||||
console.error('설비 코드 중복 확인 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 코드 중복 확인 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: '이미 사용 중인 설비 코드입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 설비 생성
|
||||
EquipmentModel.create(equipmentData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 생성 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 생성 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '설비가 성공적으로 생성되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// READ ALL - 모든 설비 조회 (필터링 가능)
|
||||
getAllEquipments: (req, res) => {
|
||||
try {
|
||||
const filters = {
|
||||
workplace_id: req.query.workplace_id,
|
||||
equipment_type: req.query.equipment_type,
|
||||
status: req.query.status,
|
||||
search: req.query.search
|
||||
};
|
||||
|
||||
EquipmentModel.getAll(filters, (error, results) => {
|
||||
if (error) {
|
||||
console.error('설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// READ ONE - 특정 설비 조회
|
||||
getEquipmentById: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getById(equipmentId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '설비를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// READ BY WORKPLACE - 특정 작업장의 설비 조회
|
||||
getEquipmentsByWorkplace: (req, res) => {
|
||||
try {
|
||||
const workplaceId = req.params.workplaceId;
|
||||
|
||||
EquipmentModel.getByWorkplace(workplaceId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('작업장 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업장 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('작업장 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// READ ACTIVE - 활성 설비만 조회
|
||||
getActiveEquipments: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getActive((error, results) => {
|
||||
if (error) {
|
||||
console.error('활성 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '활성 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('활성 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UPDATE - 설비 수정
|
||||
updateEquipment: async (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const equipmentData = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!equipmentData.equipment_code || !equipmentData.equipment_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '설비 코드와 설비명은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 설비 존재 확인
|
||||
EquipmentModel.getById(equipmentId, (error, existingEquipment) => {
|
||||
if (error) {
|
||||
console.error('설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!existingEquipment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '설비를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 설비 코드 중복 확인 (자신 제외)
|
||||
EquipmentModel.checkDuplicateCode(equipmentData.equipment_code, equipmentId, (error, isDuplicate) => {
|
||||
if (error) {
|
||||
console.error('설비 코드 중복 확인 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 코드 중복 확인 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: '이미 사용 중인 설비 코드입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 설비 수정
|
||||
EquipmentModel.update(equipmentId, equipmentData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 수정 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 수정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 성공적으로 수정되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UPDATE MAP POSITION - 지도상 위치 업데이트
|
||||
updateMapPosition: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const positionData = {
|
||||
map_x_percent: req.body.map_x_percent,
|
||||
map_y_percent: req.body.map_y_percent,
|
||||
map_width_percent: req.body.map_width_percent,
|
||||
map_height_percent: req.body.map_height_percent
|
||||
};
|
||||
|
||||
// workplace_id가 있으면 포함 (설비를 다른 작업장으로 이동 가능)
|
||||
if (req.body.workplace_id !== undefined) {
|
||||
positionData.workplace_id = req.body.workplace_id;
|
||||
}
|
||||
|
||||
EquipmentModel.updateMapPosition(equipmentId, positionData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 위치 업데이트 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 위치 업데이트 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비 위치가 성공적으로 업데이트되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 위치 업데이트 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE - 설비 삭제
|
||||
deleteEquipment: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.delete(equipmentId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 삭제 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 성공적으로 삭제되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회
|
||||
getEquipmentTypes: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getEquipmentTypes((error, results) => {
|
||||
if (error) {
|
||||
console.error('설비 유형 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 유형 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 유형 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성
|
||||
getNextEquipmentCode: (req, res) => {
|
||||
try {
|
||||
const prefix = req.query.prefix || 'TKP';
|
||||
|
||||
EquipmentModel.getNextEquipmentCode(prefix, (error, nextCode) => {
|
||||
if (error) {
|
||||
console.error('다음 관리번호 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '다음 관리번호 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
next_code: nextCode,
|
||||
prefix: prefix
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('다음 관리번호 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 사진 관리
|
||||
// ==========================================
|
||||
|
||||
// ADD PHOTO - 설비 사진 추가
|
||||
addPhoto: async (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const { photo_base64, description, display_order } = req.body;
|
||||
|
||||
if (!photo_base64) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '사진 데이터가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// Base64 이미지를 파일로 저장
|
||||
const photoPath = await imageUploadService.saveBase64Image(
|
||||
photo_base64,
|
||||
'equipment',
|
||||
'equipments'
|
||||
);
|
||||
|
||||
if (!photoPath) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 저장에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// DB에 사진 정보 저장
|
||||
const photoData = {
|
||||
photo_path: photoPath,
|
||||
description: description || null,
|
||||
display_order: display_order || 0,
|
||||
uploaded_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.addPhoto(equipmentId, photoData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('사진 정보 저장 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 정보 저장 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '사진이 성공적으로 추가되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 추가 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET PHOTOS - 설비 사진 조회
|
||||
getPhotos: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getPhotos(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('사진 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE PHOTO - 설비 사진 삭제
|
||||
deletePhoto: async (req, res) => {
|
||||
try {
|
||||
const photoId = req.params.photoId;
|
||||
|
||||
EquipmentModel.deletePhoto(photoId, async (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Photo not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '사진을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('사진 삭제 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 파일 시스템에서 사진 삭제
|
||||
if (result.photo_path) {
|
||||
await imageUploadService.deleteFile(result.photo_path);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사진이 성공적으로 삭제되었습니다.',
|
||||
data: { photo_id: photoId }
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 임시 이동
|
||||
// ==========================================
|
||||
|
||||
// MOVE TEMPORARILY - 설비 임시 이동
|
||||
moveTemporarily: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const moveData = {
|
||||
target_workplace_id: req.body.target_workplace_id,
|
||||
target_x_percent: req.body.target_x_percent,
|
||||
target_y_percent: req.body.target_y_percent,
|
||||
target_width_percent: req.body.target_width_percent,
|
||||
target_height_percent: req.body.target_height_percent,
|
||||
from_workplace_id: req.body.from_workplace_id,
|
||||
from_x_percent: req.body.from_x_percent,
|
||||
from_y_percent: req.body.from_y_percent,
|
||||
reason: req.body.reason,
|
||||
moved_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
if (!moveData.target_workplace_id || moveData.target_x_percent === undefined || moveData.target_y_percent === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이동할 작업장과 위치가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
EquipmentModel.moveTemporarily(equipmentId, moveData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 이동 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 이동 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 임시 이동되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 이동 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN TO ORIGINAL - 설비 원위치 복귀
|
||||
returnToOriginal: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const userId = req.user?.user_id || null;
|
||||
|
||||
EquipmentModel.returnToOriginal(equipmentId, userId, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Equipment not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '설비를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('설비 복귀 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 복귀 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 원위치로 복귀되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 복귀 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET TEMPORARILY MOVED - 임시 이동된 설비 목록
|
||||
getTemporarilyMoved: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getTemporarilyMoved((error, results) => {
|
||||
if (error) {
|
||||
console.error('임시 이동 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '임시 이동 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('임시 이동 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET MOVE LOGS - 설비 이동 이력 조회
|
||||
getMoveLogs: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getMoveLogs(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('이동 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '이동 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('이동 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 외부 반출/반입
|
||||
// ==========================================
|
||||
|
||||
// EXPORT EQUIPMENT - 설비 외부 반출
|
||||
exportEquipment: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const exportData = {
|
||||
equipment_id: equipmentId,
|
||||
export_date: req.body.export_date,
|
||||
expected_return_date: req.body.expected_return_date,
|
||||
destination: req.body.destination,
|
||||
reason: req.body.reason,
|
||||
notes: req.body.notes,
|
||||
is_repair: req.body.is_repair || false,
|
||||
exported_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.exportEquipment(exportData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 반출 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 반출 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '설비가 외부로 반출되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 반출 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN EQUIPMENT - 설비 반입 (외부에서 복귀)
|
||||
returnEquipment: (req, res) => {
|
||||
try {
|
||||
const logId = req.params.logId;
|
||||
const returnData = {
|
||||
return_date: req.body.return_date,
|
||||
new_status: req.body.new_status || 'active',
|
||||
notes: req.body.notes,
|
||||
returned_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.returnEquipment(logId, returnData, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Export log not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '반출 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('설비 반입 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 반입 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 반입되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 반입 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXTERNAL LOGS - 설비 외부 반출 이력 조회
|
||||
getExternalLogs: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getExternalLogs(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('반출 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '반출 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('반출 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXPORTED EQUIPMENTS - 현재 외부 반출 중인 설비 목록
|
||||
getExportedEquipments: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getExportedEquipments((error, results) => {
|
||||
if (error) {
|
||||
console.error('반출 중 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '반출 중 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('반출 중 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 수리 신청
|
||||
// ==========================================
|
||||
|
||||
// CREATE REPAIR REQUEST - 수리 신청
|
||||
createRepairRequest: async (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const { photo_base64_list, description, item_id, workplace_id } = req.body;
|
||||
|
||||
// 사진 저장 (있는 경우)
|
||||
let photoPaths = [];
|
||||
if (photo_base64_list && photo_base64_list.length > 0) {
|
||||
for (const base64 of photo_base64_list) {
|
||||
const path = await imageUploadService.saveBase64Image(base64, 'repair', 'issues');
|
||||
if (path) photoPaths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
equipment_id: equipmentId,
|
||||
item_id: item_id || null,
|
||||
workplace_id: workplace_id || null,
|
||||
description: description || null,
|
||||
photo_paths: photoPaths.length > 0 ? photoPaths : null,
|
||||
reported_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.createRepairRequest(requestData, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === '설비 수리 카테고리가 없습니다') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
console.error('수리 신청 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 신청 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '수리 신청이 접수되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 신청 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR HISTORY - 설비 수리 이력 조회
|
||||
getRepairHistory: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getRepairHistory(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('수리 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR CATEGORIES - 설비 수리 항목 목록 조회
|
||||
getRepairCategories: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getRepairCategories((error, results) => {
|
||||
if (error) {
|
||||
console.error('수리 항목 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 항목 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 항목 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ADD REPAIR CATEGORY - 새 수리 항목 추가
|
||||
addRepairCategory: (req, res) => {
|
||||
try {
|
||||
const { item_name } = req.body;
|
||||
|
||||
if (!item_name || !item_name.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수리 유형 이름을 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
EquipmentModel.addRepairCategory(item_name.trim(), (error, result) => {
|
||||
if (error) {
|
||||
console.error('수리 항목 추가 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 항목 추가 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: result.isNew ? '새 수리 유형이 추가되었습니다.' : '기존 수리 유형을 사용합니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 항목 추가 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EquipmentController;
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 이슈 유형 관리 컨트롤러
|
||||
*
|
||||
* 이슈 유형(카테고리/서브카테고리) CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const issueTypeService = require('../services/issueTypeService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 이슈 유형 생성
|
||||
*/
|
||||
exports.createIssueType = asyncHandler(async (req, res) => {
|
||||
const result = await issueTypeService.createIssueTypeService(req.body);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '이슈 유형이 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 이슈 유형 조회
|
||||
*/
|
||||
exports.getAllIssueTypes = asyncHandler(async (req, res) => {
|
||||
const rows = await issueTypeService.getAllIssueTypesService();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '이슈 유형 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 이슈 유형 수정
|
||||
*/
|
||||
exports.updateIssueType = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const result = await issueTypeService.updateIssueTypeService(id, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '이슈 유형이 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 이슈 유형 삭제
|
||||
*/
|
||||
exports.removeIssueType = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const result = await issueTypeService.removeIssueTypeService(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '이슈 유형이 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 월별 작업자 상태 집계 컨트롤러
|
||||
*
|
||||
* 월별 캘린더 및 작업자 상태 집계 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const MonthlyStatusModel = require('../models/monthlyStatusModel');
|
||||
const { ValidationError, ForbiddenError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 월별 캘린더 데이터 조회
|
||||
*/
|
||||
const getMonthlyCalendarData = asyncHandler(async (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
throw new ValidationError('연도(year)와 월(month)이 필요합니다', {
|
||||
required: ['year', 'month'],
|
||||
received: { year, month }
|
||||
});
|
||||
}
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
const monthNum = parseInt(month);
|
||||
|
||||
if (yearNum < 2020 || yearNum > 2030 || monthNum < 1 || monthNum > 12) {
|
||||
throw new ValidationError('유효하지 않은 연도 또는 월입니다', {
|
||||
received: { year: yearNum, month: monthNum }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('월별 캘린더 데이터 조회 요청', { year: yearNum, month: monthNum });
|
||||
|
||||
try {
|
||||
const summaryData = await MonthlyStatusModel.getMonthlySummary(yearNum, monthNum);
|
||||
|
||||
// 날짜별 객체로 변환
|
||||
const calendarData = {};
|
||||
summaryData.forEach(day => {
|
||||
const dateKey = day.date.toISOString().split('T')[0];
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
logger.info('월별 캘린더 데이터 조회 성공', {
|
||||
year: yearNum,
|
||||
month: monthNum,
|
||||
dayCount: Object.keys(calendarData).length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: calendarData,
|
||||
message: `${year}년 ${month}월 캘린더 데이터를 성공적으로 조회했습니다`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('월별 캘린더 데이터 조회 실패', {
|
||||
year: yearNum,
|
||||
month: monthNum,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('월별 캘린더 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업자별 상세 상태 조회
|
||||
*/
|
||||
const getDailyWorkerDetails = asyncHandler(async (req, res) => {
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
throw new ValidationError('날짜(date)가 필요합니다', {
|
||||
required: ['date'],
|
||||
received: { date }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('일별 작업자 상세 조회 요청', { date });
|
||||
|
||||
try {
|
||||
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)
|
||||
};
|
||||
|
||||
logger.info('일별 작업자 상세 조회 성공', {
|
||||
date,
|
||||
workerCount: formattedData.length,
|
||||
totalHours: summary.totalHours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workers: formattedData,
|
||||
summary
|
||||
},
|
||||
message: `${date} 작업자 상세 정보를 성공적으로 조회했습니다`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('일별 작업자 상세 조회 실패', {
|
||||
date,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('일별 작업자 상세 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 월별 집계 재계산 (관리자용)
|
||||
*/
|
||||
const recalculateMonth = asyncHandler(async (req, res) => {
|
||||
const { year, month } = req.body;
|
||||
|
||||
if (!year || !month) {
|
||||
throw new ValidationError('연도(year)와 월(month)이 필요합니다', {
|
||||
required: ['year', 'month'],
|
||||
received: { year, month }
|
||||
});
|
||||
}
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
throw new ForbiddenError('관리자 권한이 필요합니다');
|
||||
}
|
||||
|
||||
logger.info('월별 집계 재계산 시작', {
|
||||
year,
|
||||
month,
|
||||
requestedBy: req.user.username
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await MonthlyStatusModel.recalculateMonth(parseInt(year), parseInt(month));
|
||||
|
||||
logger.info('월별 집계 재계산 성공', { year, month, result });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `${year}년 ${month}월 집계 재계산이 완료되었습니다`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('월별 집계 재계산 실패', {
|
||||
year,
|
||||
month,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('월별 집계 재계산 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 집계 테이블 상태 확인 (관리자용)
|
||||
*/
|
||||
const getStatusInfo = asyncHandler(async (req, res) => {
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
throw new ForbiddenError('관리자 권한이 필요합니다');
|
||||
}
|
||||
|
||||
logger.info('집계 테이블 상태 확인 요청', {
|
||||
requestedBy: req.user.username
|
||||
});
|
||||
|
||||
try {
|
||||
const statusInfo = await MonthlyStatusModel.getStatusInfo();
|
||||
|
||||
logger.info('집계 테이블 상태 확인 성공');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statusInfo,
|
||||
message: '집계 테이블 상태 정보를 성공적으로 조회했습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('집계 테이블 상태 확인 실패', {
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('집계 테이블 상태 확인 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getMonthlyCalendarData,
|
||||
getDailyWorkerDetails,
|
||||
recalculateMonth,
|
||||
getStatusInfo
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
// controllers/notificationController.js
|
||||
const notificationModel = require('../models/notificationModel');
|
||||
|
||||
const notificationController = {
|
||||
// 읽지 않은 알림 조회
|
||||
async getUnread(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || null;
|
||||
const notifications = await notificationModel.getUnread(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: notifications
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('읽지 않은 알림 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 전체 알림 조회
|
||||
async getAll(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || null;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
|
||||
const result = await notificationModel.getAll(userId, page, limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.notifications,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: Math.ceil(result.total / result.limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('알림 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 읽지 않은 알림 개수
|
||||
async getUnreadCount(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || null;
|
||||
const count = await notificationModel.getUnreadCount(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { count }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('알림 개수 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 개수 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 읽음 처리
|
||||
async markAsRead(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await notificationModel.markAsRead(id);
|
||||
|
||||
res.json({
|
||||
success,
|
||||
message: success ? '알림을 읽음 처리했습니다.' : '알림을 찾을 수 없습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('알림 읽음 처리 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 처리 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 모든 알림 읽음 처리
|
||||
async markAllAsRead(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || null;
|
||||
const count = await notificationModel.markAllAsRead(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${count}개의 알림을 읽음 처리했습니다.`,
|
||||
data: { count }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('전체 읽음 처리 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 처리 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 삭제
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const success = await notificationModel.delete(id);
|
||||
|
||||
res.json({
|
||||
success,
|
||||
message: success ? '알림을 삭제했습니다.' : '알림을 찾을 수 없습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('알림 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 생성 (시스템용)
|
||||
async create(req, res) {
|
||||
try {
|
||||
const { type, title, message, link_url, user_id } = req.body;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '알림 제목은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const notificationId = await notificationModel.create({
|
||||
user_id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
link_url,
|
||||
created_by: req.user?.id
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '알림이 생성되었습니다.',
|
||||
data: { notification_id: notificationId }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('알림 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '알림 생성 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationController;
|
||||
@@ -0,0 +1,91 @@
|
||||
// controllers/notificationRecipientController.js
|
||||
const notificationRecipientModel = require('../models/notificationRecipientModel');
|
||||
|
||||
const notificationRecipientController = {
|
||||
// 알림 유형 목록
|
||||
getTypes: async (req, res) => {
|
||||
try {
|
||||
const types = notificationRecipientModel.getTypes();
|
||||
res.json({ success: true, data: types });
|
||||
} catch (error) {
|
||||
console.error('알림 유형 조회 오류:', error);
|
||||
res.status(500).json({ success: false, error: '알림 유형 조회 실패' });
|
||||
}
|
||||
},
|
||||
|
||||
// 전체 수신자 목록 (유형별 그룹화)
|
||||
getAll: async (req, res) => {
|
||||
try {
|
||||
console.log('🔔 알림 수신자 목록 조회 시작');
|
||||
const recipients = await notificationRecipientModel.getAll();
|
||||
console.log('✅ 알림 수신자 목록 조회 완료:', recipients);
|
||||
res.json({ success: true, data: recipients });
|
||||
} catch (error) {
|
||||
console.error('❌ 수신자 목록 조회 오류:', error.message);
|
||||
console.error('❌ 스택:', error.stack);
|
||||
res.status(500).json({ success: false, error: '수신자 목록 조회 실패', detail: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
// 유형별 수신자 조회
|
||||
getByType: async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const recipients = await notificationRecipientModel.getByType(type);
|
||||
res.json({ success: true, data: recipients });
|
||||
} catch (error) {
|
||||
console.error('수신자 조회 오류:', error);
|
||||
res.status(500).json({ success: false, error: '수신자 조회 실패' });
|
||||
}
|
||||
},
|
||||
|
||||
// 수신자 추가
|
||||
add: async (req, res) => {
|
||||
try {
|
||||
const { notification_type, user_id } = req.body;
|
||||
|
||||
if (!notification_type || !user_id) {
|
||||
return res.status(400).json({ success: false, error: '알림 유형과 사용자 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
await notificationRecipientModel.add(notification_type, user_id, req.user?.user_id);
|
||||
res.json({ success: true, message: '수신자가 추가되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('수신자 추가 오류:', error);
|
||||
res.status(500).json({ success: false, error: '수신자 추가 실패' });
|
||||
}
|
||||
},
|
||||
|
||||
// 수신자 제거
|
||||
remove: async (req, res) => {
|
||||
try {
|
||||
const { type, userId } = req.params;
|
||||
|
||||
await notificationRecipientModel.remove(type, userId);
|
||||
res.json({ success: true, message: '수신자가 제거되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('수신자 제거 오류:', error);
|
||||
res.status(500).json({ success: false, error: '수신자 제거 실패' });
|
||||
}
|
||||
},
|
||||
|
||||
// 유형별 수신자 일괄 설정
|
||||
setRecipients: async (req, res) => {
|
||||
try {
|
||||
const { type } = req.params;
|
||||
const { user_ids } = req.body;
|
||||
|
||||
if (!Array.isArray(user_ids)) {
|
||||
return res.status(400).json({ success: false, error: 'user_ids 배열이 필요합니다.' });
|
||||
}
|
||||
|
||||
await notificationRecipientModel.setRecipients(type, user_ids, req.user?.user_id);
|
||||
res.json({ success: true, message: '수신자가 설정되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('수신자 설정 오류:', error);
|
||||
res.status(500).json({ success: false, error: '수신자 설정 실패' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationRecipientController;
|
||||
@@ -0,0 +1,200 @@
|
||||
// controllers/pageAccessController.js
|
||||
const PageAccessModel = require('../models/pageAccessModel');
|
||||
|
||||
const PageAccessController = {
|
||||
// 사용자의 페이지 권한 조회
|
||||
getUserPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
|
||||
if (isNaN(userId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 사용자 ID입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.getUserPageAccess(userId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 모든 페이지 목록 조회
|
||||
getAllPages: (req, res) => {
|
||||
PageAccessModel.getAllPages((err, results) => {
|
||||
if (err) {
|
||||
console.error('페이지 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 페이지 권한 부여
|
||||
grantPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const { pageId } = req.body;
|
||||
const grantedBy = req.user.user_id;
|
||||
|
||||
if (isNaN(userId) || !pageId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 파라미터가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.grantPageAccess(userId, pageId, grantedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 부여 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 부여 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 부여되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 페이지 권한 회수
|
||||
revokePageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const pageId = parseInt(req.params.pageId);
|
||||
|
||||
if (isNaN(userId) || isNaN(pageId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 파라미터입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.revokePageAccess(userId, pageId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 회수 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 회수 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 회수되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 사용자 페이지 권한 일괄 설정
|
||||
setUserPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const { pageIds } = req.body;
|
||||
const grantedBy = req.user.user_id;
|
||||
|
||||
if (isNaN(userId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 사용자 ID입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(pageIds)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'pageIds는 배열이어야 합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.setUserPageAccess(userId, pageIds, grantedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 설정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 설정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 설정되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 특정 페이지 접근 권한 확인
|
||||
checkPageAccess: (req, res) => {
|
||||
const userId = req.user.user_id;
|
||||
const { pageKey } = req.params;
|
||||
|
||||
if (!pageKey) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '페이지 키가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.checkPageAccess(userId, pageKey, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 접근 권한 확인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 접근 권한 확인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 계정이 있는 사용자 목록 조회 (권한 관리용)
|
||||
getUsersWithAccounts: (req, res) => {
|
||||
PageAccessModel.getUsersWithAccounts((err, results) => {
|
||||
if (err) {
|
||||
console.error('사용자 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사용자 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PageAccessController;
|
||||
@@ -0,0 +1,796 @@
|
||||
// patrolController.js
|
||||
// 일일순회점검 시스템 컨트롤러
|
||||
|
||||
const PatrolModel = require('../models/patrolModel');
|
||||
|
||||
const PatrolController = {
|
||||
// ==================== 순회점검 세션 ====================
|
||||
|
||||
// 세션 시작/조회
|
||||
getOrCreateSession: async (req, res) => {
|
||||
try {
|
||||
const { patrol_date, patrol_time, category_id } = req.body;
|
||||
const inspectorId = req.user.user_id;
|
||||
|
||||
if (!patrol_date || !patrol_time || !category_id) {
|
||||
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
const session = await PatrolModel.getOrCreateSession(patrol_date, patrol_time, category_id, inspectorId);
|
||||
res.json({ success: true, data: session });
|
||||
} catch (error) {
|
||||
console.error('세션 생성/조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 세션 상세 조회
|
||||
getSession: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const session = await PatrolModel.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ success: false, message: '세션을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: session });
|
||||
} catch (error) {
|
||||
console.error('세션 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 세션 목록 조회
|
||||
getSessions: async (req, res) => {
|
||||
try {
|
||||
const { patrol_date, patrol_time, category_id, status, limit } = req.query;
|
||||
const sessions = await PatrolModel.getSessions({
|
||||
patrol_date,
|
||||
patrol_time,
|
||||
category_id,
|
||||
status,
|
||||
limit
|
||||
});
|
||||
res.json({ success: true, data: sessions });
|
||||
} catch (error) {
|
||||
console.error('세션 목록 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 세션 완료
|
||||
completeSession: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
await PatrolModel.completeSession(sessionId);
|
||||
res.json({ success: true, message: '순회점검이 완료되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('세션 완료 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 세션 메모 업데이트
|
||||
updateSessionNotes: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { notes } = req.body;
|
||||
await PatrolModel.updateSessionNotes(sessionId, notes);
|
||||
res.json({ success: true, message: '메모가 저장되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('메모 저장 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 체크리스트 항목 ====================
|
||||
|
||||
// 체크리스트 항목 조회
|
||||
getChecklistItems: async (req, res) => {
|
||||
try {
|
||||
const { category_id, workplace_id } = req.query;
|
||||
const items = await PatrolModel.getChecklistItems(category_id, workplace_id);
|
||||
|
||||
// 카테고리별로 그룹화
|
||||
const grouped = {};
|
||||
items.forEach(item => {
|
||||
if (!grouped[item.check_category]) {
|
||||
grouped[item.check_category] = [];
|
||||
}
|
||||
grouped[item.check_category].push(item);
|
||||
});
|
||||
|
||||
res.json({ success: true, data: { items, grouped } });
|
||||
} catch (error) {
|
||||
console.error('체크리스트 항목 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 체크리스트 항목 추가
|
||||
createChecklistItem: async (req, res) => {
|
||||
try {
|
||||
const itemId = await PatrolModel.createChecklistItem(req.body);
|
||||
res.json({ success: true, data: { item_id: itemId }, message: '항목이 추가되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('항목 추가 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 체크리스트 항목 수정
|
||||
updateChecklistItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
await PatrolModel.updateChecklistItem(itemId, req.body);
|
||||
res.json({ success: true, message: '항목이 수정되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('항목 수정 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 체크리스트 항목 삭제
|
||||
deleteChecklistItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
await PatrolModel.deleteChecklistItem(itemId);
|
||||
res.json({ success: true, message: '항목이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('항목 삭제 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 체크 기록 ====================
|
||||
|
||||
// 작업장별 체크 기록 조회
|
||||
getCheckRecords: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { workplace_id } = req.query;
|
||||
const records = await PatrolModel.getCheckRecords(sessionId, workplace_id);
|
||||
res.json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
console.error('체크 기록 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 체크 기록 저장
|
||||
saveCheckRecord: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { workplace_id, check_item_id, is_checked, check_result, note } = req.body;
|
||||
|
||||
if (!workplace_id || !check_item_id) {
|
||||
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
await PatrolModel.saveCheckRecord(sessionId, workplace_id, check_item_id, is_checked, check_result, note);
|
||||
res.json({ success: true, message: '저장되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('체크 기록 저장 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 체크 기록 일괄 저장
|
||||
saveCheckRecords: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { workplace_id, records } = req.body;
|
||||
|
||||
if (!workplace_id || !records || !Array.isArray(records)) {
|
||||
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
await PatrolModel.saveCheckRecords(sessionId, workplace_id, records);
|
||||
res.json({ success: true, message: '저장되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('체크 기록 일괄 저장 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 작업장 물품 현황 ====================
|
||||
|
||||
// 작업장 물품 조회
|
||||
getWorkplaceItems: async (req, res) => {
|
||||
try {
|
||||
const { workplaceId } = req.params;
|
||||
const { include_inactive } = req.query;
|
||||
const items = await PatrolModel.getWorkplaceItems(workplaceId, include_inactive !== 'true');
|
||||
res.json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
console.error('물품 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 물품 추가
|
||||
createWorkplaceItem: async (req, res) => {
|
||||
try {
|
||||
const { workplaceId } = req.params;
|
||||
const data = { ...req.body, workplace_id: workplaceId, created_by: req.user.user_id };
|
||||
const itemId = await PatrolModel.createWorkplaceItem(data);
|
||||
res.json({ success: true, data: { item_id: itemId }, message: '물품이 추가되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('물품 추가 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 물품 수정
|
||||
updateWorkplaceItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
await PatrolModel.updateWorkplaceItem(itemId, req.body, req.user.user_id);
|
||||
res.json({ success: true, message: '물품이 수정되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('물품 수정 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 물품 삭제
|
||||
deleteWorkplaceItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const { permanent } = req.query;
|
||||
|
||||
if (permanent === 'true') {
|
||||
await PatrolModel.hardDeleteWorkplaceItem(itemId);
|
||||
} else {
|
||||
await PatrolModel.deleteWorkplaceItem(itemId, req.user.user_id);
|
||||
}
|
||||
res.json({ success: true, message: '물품이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('물품 삭제 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 물품 유형 ====================
|
||||
|
||||
// 물품 유형 목록
|
||||
getItemTypes: async (req, res) => {
|
||||
try {
|
||||
const types = await PatrolModel.getItemTypes();
|
||||
res.json({ success: true, data: types });
|
||||
} catch (error) {
|
||||
console.error('물품 유형 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 대시보드/통계 ====================
|
||||
|
||||
// 오늘 순회점검 현황
|
||||
getTodayStatus: async (req, res) => {
|
||||
try {
|
||||
const { category_id } = req.query;
|
||||
const status = await PatrolModel.getTodayPatrolStatus(category_id);
|
||||
res.json({ success: true, data: status });
|
||||
} catch (error) {
|
||||
console.error('오늘 현황 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 작업장별 점검 현황
|
||||
getWorkplaceCheckStatus: async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const status = await PatrolModel.getWorkplaceCheckStatus(sessionId);
|
||||
res.json({ success: true, data: status });
|
||||
} catch (error) {
|
||||
console.error('작업장별 점검 현황 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 작업장 상세 정보 (통합) ====================
|
||||
|
||||
// 작업장 상세 정보 조회 (시설물, 안전신고, 부적합, 출입, TBM)
|
||||
getWorkplaceDetail: async (req, res) => {
|
||||
try {
|
||||
const { workplaceId } = req.params;
|
||||
const { date } = req.query; // 기본: 오늘
|
||||
const targetDate = date || new Date().toISOString().slice(0, 10);
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 작업장 기본 정보 (카테고리 지도 이미지 포함)
|
||||
const [workplaceInfo] = await db.query(`
|
||||
SELECT w.*, wc.category_name, wc.layout_image as category_layout_image
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE w.workplace_id = ?
|
||||
`, [workplaceId]);
|
||||
|
||||
if (!workplaceInfo.length) {
|
||||
return res.status(404).json({ success: false, message: '작업장을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 2. 설비 현황 (해당 작업장 - 원래 위치 또는 현재 위치)
|
||||
let equipments = [];
|
||||
try {
|
||||
const [eqResult] = await db.query(`
|
||||
SELECT e.equipment_id, e.equipment_name, e.equipment_code, e.equipment_type,
|
||||
e.status, e.notes, e.workplace_id,
|
||||
e.map_x_percent, e.map_y_percent, e.map_width_percent, e.map_height_percent,
|
||||
e.is_temporarily_moved, e.current_workplace_id,
|
||||
e.current_map_x_percent, e.current_map_y_percent,
|
||||
e.current_map_width_percent, e.current_map_height_percent,
|
||||
e.moved_at,
|
||||
ow.workplace_name as original_workplace_name,
|
||||
cw.workplace_name as current_workplace_name,
|
||||
CASE
|
||||
WHEN e.status IN ('maintenance', 'repair_needed', 'repair_external') THEN 1
|
||||
WHEN e.is_temporarily_moved = 1 THEN 1
|
||||
ELSE 0
|
||||
END as needs_attention
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces ow ON e.workplace_id = ow.workplace_id
|
||||
LEFT JOIN workplaces cw ON e.current_workplace_id = cw.workplace_id
|
||||
WHERE (e.workplace_id = ? OR e.current_workplace_id = ?)
|
||||
AND e.status != 'inactive'
|
||||
ORDER BY needs_attention DESC, e.equipment_name
|
||||
`, [workplaceId, workplaceId]);
|
||||
equipments = eqResult;
|
||||
} catch (eqError) {
|
||||
console.log('설비 조회 스킵 (테이블 없음 또는 오류):', eqError.message);
|
||||
}
|
||||
|
||||
// 3. 수리 요청 현황 (미완료) - 테이블 존재 여부 확인 후 조회
|
||||
let repairRequests = [];
|
||||
try {
|
||||
const [repairResult] = await db.query(`
|
||||
SELECT er.request_id, er.request_date, er.repair_category, er.description,
|
||||
er.priority, er.status, e.equipment_name, e.equipment_code
|
||||
FROM equipment_repair_requests er
|
||||
JOIN equipments e ON er.equipment_id = e.equipment_id
|
||||
WHERE e.workplace_id = ? AND er.status NOT IN ('completed', 'cancelled')
|
||||
ORDER BY
|
||||
CASE er.priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END,
|
||||
er.request_date DESC
|
||||
LIMIT 10
|
||||
`, [workplaceId]);
|
||||
repairRequests = repairResult;
|
||||
} catch (repairError) {
|
||||
console.log('수리요청 조회 스킵 (테이블 없음 또는 오류):', repairError.message);
|
||||
}
|
||||
|
||||
// 4. 안전 신고 및 부적합 사항 - 테이블 존재 여부 확인 후 조회
|
||||
let workIssues = [];
|
||||
try {
|
||||
const [issueResult] = await db.query(`
|
||||
SELECT wi.report_id, wi.issue_type, wi.title, wi.description,
|
||||
wi.status, wi.severity, wi.created_at, wi.resolved_at,
|
||||
wic.category_name, wic.issue_type as category_type,
|
||||
u.name as reporter_name
|
||||
FROM work_issue_reports wi
|
||||
LEFT JOIN work_issue_categories wic ON wi.category_id = wic.category_id
|
||||
LEFT JOIN Users u ON wi.reporter_id = u.user_id
|
||||
WHERE wi.workplace_id = ?
|
||||
AND wi.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
ORDER BY wi.created_at DESC
|
||||
LIMIT 20
|
||||
`, [workplaceId]);
|
||||
workIssues = issueResult;
|
||||
} catch (issueError) {
|
||||
console.log('신고 조회 스킵 (테이블 없음 또는 오류):', issueError.message);
|
||||
}
|
||||
|
||||
// 5. 오늘의 출입 기록 (해당 공장 카테고리)
|
||||
const categoryId = workplaceInfo[0].category_id;
|
||||
let visitRecords = [];
|
||||
try {
|
||||
const [visitResult] = await db.query(`
|
||||
SELECT vr.request_id, vr.visitor_name, vr.visitor_company, vr.visit_purpose,
|
||||
vr.visit_date, vr.visit_time_from, vr.visit_time_to, vr.status,
|
||||
vr.vehicle_number, vr.companion_count,
|
||||
vp.purpose_name, u.name as requester_name
|
||||
FROM visit_requests vr
|
||||
LEFT JOIN visit_purposes vp ON vr.purpose_id = vp.purpose_id
|
||||
LEFT JOIN Users u ON vr.requester_id = u.user_id
|
||||
WHERE vr.category_id = ? AND vr.visit_date = ? AND vr.status = 'approved'
|
||||
ORDER BY vr.visit_time_from
|
||||
`, [categoryId, targetDate]);
|
||||
visitRecords = visitResult;
|
||||
} catch (visitError) {
|
||||
console.log('출입기록 조회 스킵 (테이블 없음 또는 오류):', visitError.message);
|
||||
}
|
||||
|
||||
// 6. 오늘의 TBM 세션 (해당 공장 카테고리)
|
||||
let tbmSessions = [];
|
||||
try {
|
||||
const [tbmResult] = await db.query(`
|
||||
SELECT ts.session_id, ts.session_date, ts.work_location, ts.status,
|
||||
ts.work_content, ts.safety_measures, ts.team_size,
|
||||
t.task_name, wt.name as work_type_name,
|
||||
u.name as leader_name, w.worker_name as leader_worker_name
|
||||
FROM tbm_sessions ts
|
||||
LEFT JOIN tasks t ON ts.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN Users u ON ts.leader_id = u.user_id
|
||||
LEFT JOIN workers w ON ts.leader_worker_id = w.worker_id
|
||||
WHERE ts.category_id = ? AND ts.session_date = ?
|
||||
ORDER BY ts.created_at DESC
|
||||
`, [categoryId, targetDate]);
|
||||
tbmSessions = tbmResult;
|
||||
} catch (tbmError) {
|
||||
console.log('TBM 조회 스킵 (테이블 없음 또는 오류):', tbmError.message);
|
||||
}
|
||||
|
||||
// 7. TBM 팀원 정보 (세션별)
|
||||
let tbmWithTeams = [];
|
||||
try {
|
||||
tbmWithTeams = await Promise.all(tbmSessions.map(async (session) => {
|
||||
const [team] = await db.query(`
|
||||
SELECT tta.assignment_id, w.worker_name, w.occupation,
|
||||
tta.attendance_status, tta.signature_image
|
||||
FROM tbm_team_assignments tta
|
||||
JOIN workers w ON tta.worker_id = w.worker_id
|
||||
WHERE tta.session_id = ?
|
||||
ORDER BY w.worker_name
|
||||
`, [session.session_id]);
|
||||
return { ...session, team };
|
||||
}));
|
||||
} catch (teamError) {
|
||||
console.log('TBM 팀원 조회 스킵:', teamError.message);
|
||||
tbmWithTeams = tbmSessions.map(s => ({ ...s, team: [] }));
|
||||
}
|
||||
|
||||
// 8. 최근 순회점검 결과 (해당 작업장)
|
||||
let recentPatrol = [];
|
||||
try {
|
||||
const [patrolResult] = await db.query(`
|
||||
SELECT ps.session_id, ps.patrol_date, ps.patrol_time, ps.status,
|
||||
ps.notes, u.name as inspector_name,
|
||||
(SELECT COUNT(*) FROM patrol_check_records pcr
|
||||
WHERE pcr.session_id = ps.session_id AND pcr.workplace_id = ?) as checked_count,
|
||||
(SELECT COUNT(*) FROM patrol_check_records pcr
|
||||
WHERE pcr.session_id = ps.session_id AND pcr.workplace_id = ?
|
||||
AND pcr.check_result IN ('warning', 'bad')) as issue_count
|
||||
FROM patrol_sessions ps
|
||||
LEFT JOIN Users u ON ps.inspector_id = u.user_id
|
||||
WHERE ps.category_id = ? AND ps.patrol_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
ORDER BY ps.patrol_date DESC, ps.patrol_time DESC
|
||||
LIMIT 5
|
||||
`, [workplaceId, workplaceId, categoryId]);
|
||||
recentPatrol = patrolResult;
|
||||
} catch (patrolError) {
|
||||
console.log('순회점검 조회 스킵 (테이블 없음 또는 오류):', patrolError.message);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workplace: workplaceInfo[0],
|
||||
equipments: equipments,
|
||||
repairRequests: repairRequests,
|
||||
workIssues: {
|
||||
safety: workIssues.filter(i => i.category_type === 'safety'),
|
||||
nonconformity: workIssues.filter(i => i.category_type === 'nonconformity'),
|
||||
all: workIssues
|
||||
},
|
||||
visitRecords: visitRecords,
|
||||
tbmSessions: tbmWithTeams,
|
||||
recentPatrol: recentPatrol,
|
||||
summary: {
|
||||
equipmentCount: equipments.length,
|
||||
needsAttention: equipments.filter(e => e.needs_attention).length,
|
||||
pendingRepairs: repairRequests.length,
|
||||
openIssues: workIssues.filter(i => i.status !== 'closed').length,
|
||||
todayVisitors: visitRecords.reduce((sum, v) => sum + 1 + (v.companion_count || 0), 0),
|
||||
todayTbmSessions: tbmSessions.length
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('작업장 상세 정보 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 구역 내 등록 물품/시설물 ====================
|
||||
|
||||
// 구역 내 물품/시설물 목록 조회
|
||||
getZoneItems: async (req, res) => {
|
||||
try {
|
||||
const { workplaceId } = req.params;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 테이블이 없으면 생성
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS workplace_zone_items (
|
||||
item_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
workplace_id INT NOT NULL,
|
||||
item_name VARCHAR(200) NOT NULL COMMENT '물품/시설물 명칭',
|
||||
item_type VARCHAR(50) DEFAULT 'general' COMMENT '유형 (heavy_equipment, hazardous, storage, general 등)',
|
||||
description TEXT COMMENT '상세 설명',
|
||||
x_percent DECIMAL(5,2) NOT NULL COMMENT '영역 시작 X 좌표 (%)',
|
||||
y_percent DECIMAL(5,2) NOT NULL COMMENT '영역 시작 Y 좌표 (%)',
|
||||
width_percent DECIMAL(5,2) DEFAULT 10 COMMENT '영역 너비 (%)',
|
||||
height_percent DECIMAL(5,2) DEFAULT 10 COMMENT '영역 높이 (%)',
|
||||
color VARCHAR(20) DEFAULT '#3b82f6' COMMENT '표시 색상',
|
||||
warning_level VARCHAR(20) DEFAULT 'normal' COMMENT '주의 수준 (normal, caution, danger)',
|
||||
quantity INT DEFAULT 1 COMMENT '수량',
|
||||
unit VARCHAR(20) DEFAULT '개' COMMENT '단위',
|
||||
weight_kg DECIMAL(10,2) DEFAULT NULL COMMENT '중량 (kg)',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_workplace (workplace_id),
|
||||
INDEX idx_type (item_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='구역 내 등록 물품/시설물'
|
||||
`);
|
||||
|
||||
// 새 컬럼 추가 (없으면)
|
||||
try {
|
||||
await db.query(`ALTER TABLE workplace_zone_items ADD COLUMN project_type VARCHAR(20) DEFAULT 'non_project'`);
|
||||
} catch (e) { /* 이미 존재 */ }
|
||||
try {
|
||||
await db.query(`ALTER TABLE workplace_zone_items ADD COLUMN project_id INT NULL`);
|
||||
} catch (e) { /* 이미 존재 */ }
|
||||
|
||||
const [items] = await db.query(`
|
||||
SELECT zi.*, p.project_name
|
||||
FROM workplace_zone_items zi
|
||||
LEFT JOIN projects p ON zi.project_id = p.project_id
|
||||
WHERE zi.workplace_id = ? AND zi.is_active = TRUE
|
||||
ORDER BY zi.warning_level DESC, zi.item_name
|
||||
`, [workplaceId]);
|
||||
|
||||
// 사진 테이블 존재 확인 및 사진 조회
|
||||
try {
|
||||
for (const item of items) {
|
||||
const [photos] = await db.query(`
|
||||
SELECT photo_id, photo_url, created_at
|
||||
FROM zone_item_photos
|
||||
WHERE item_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [item.item_id]);
|
||||
item.photos = photos || [];
|
||||
}
|
||||
} catch (e) {
|
||||
// 사진 테이블이 없으면 무시
|
||||
items.forEach(item => item.photos = []);
|
||||
}
|
||||
|
||||
res.json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
console.error('구역 물품 목록 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구역 현황 등록
|
||||
createZoneItem: async (req, res) => {
|
||||
try {
|
||||
const { workplaceId } = req.params;
|
||||
const { item_name, item_type, description, x_percent, y_percent, width_percent, height_percent,
|
||||
color, warning_level, project_type, project_id } = req.body;
|
||||
const createdBy = req.user?.user_id;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
if (!item_name || x_percent === undefined || y_percent === undefined) {
|
||||
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
// 테이블에 새 컬럼 추가 (없으면)
|
||||
try {
|
||||
await db.query(`ALTER TABLE workplace_zone_items ADD COLUMN project_type VARCHAR(20) DEFAULT 'non_project'`);
|
||||
} catch (e) { /* 이미 존재 */ }
|
||||
try {
|
||||
await db.query(`ALTER TABLE workplace_zone_items ADD COLUMN project_id INT NULL`);
|
||||
} catch (e) { /* 이미 존재 */ }
|
||||
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO workplace_zone_items
|
||||
(workplace_id, item_name, item_type, description, x_percent, y_percent, width_percent, height_percent,
|
||||
color, warning_level, project_type, project_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [workplaceId, item_name, item_type || 'working', description, x_percent, y_percent,
|
||||
width_percent || 5, height_percent || 5, color || '#3b82f6', warning_level || 'good',
|
||||
project_type || 'non_project', project_id || null, createdBy]);
|
||||
|
||||
const newItemId = result.insertId;
|
||||
|
||||
// 등록 이력 저장
|
||||
try {
|
||||
await db.query(`
|
||||
INSERT INTO zone_item_history (item_id, action_type, new_values, changed_by)
|
||||
VALUES (?, 'created', ?, ?)
|
||||
`, [newItemId, JSON.stringify({ item_name, item_type, warning_level, project_type }), createdBy]);
|
||||
} catch (e) { /* 테이블 없으면 무시 */ }
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { item_id: newItemId },
|
||||
message: '현황이 등록되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('구역 현황 등록 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구역 현황 수정
|
||||
updateZoneItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const { item_name, item_type, description, x_percent, y_percent, width_percent, height_percent,
|
||||
color, warning_level, project_type, project_id } = req.body;
|
||||
const userId = req.user?.user_id;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 이력 테이블 생성 (없으면)
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS zone_item_history (
|
||||
history_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
item_id INT NOT NULL,
|
||||
action_type VARCHAR(20) NOT NULL COMMENT 'created, updated, deleted',
|
||||
changed_fields TEXT COMMENT '변경된 필드 JSON',
|
||||
old_values TEXT COMMENT '이전 값 JSON',
|
||||
new_values TEXT COMMENT '새 값 JSON',
|
||||
changed_by INT,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_item (item_id),
|
||||
INDEX idx_date (changed_at)
|
||||
)
|
||||
`);
|
||||
|
||||
// 기존 데이터 조회 (이력용)
|
||||
const [oldData] = await db.query(`SELECT * FROM workplace_zone_items WHERE item_id = ?`, [itemId]);
|
||||
const oldItem = oldData[0];
|
||||
|
||||
// 업데이트
|
||||
await db.query(`
|
||||
UPDATE workplace_zone_items SET
|
||||
item_name = COALESCE(?, item_name),
|
||||
item_type = COALESCE(?, item_type),
|
||||
description = ?,
|
||||
x_percent = COALESCE(?, x_percent),
|
||||
y_percent = COALESCE(?, y_percent),
|
||||
width_percent = COALESCE(?, width_percent),
|
||||
height_percent = COALESCE(?, height_percent),
|
||||
color = COALESCE(?, color),
|
||||
warning_level = COALESCE(?, warning_level),
|
||||
project_type = COALESCE(?, project_type),
|
||||
project_id = ?
|
||||
WHERE item_id = ?
|
||||
`, [item_name, item_type, description, x_percent, y_percent, width_percent, height_percent,
|
||||
color, warning_level, project_type, project_id, itemId]);
|
||||
|
||||
// 변경 이력 저장
|
||||
if (oldItem) {
|
||||
const changedFields = [];
|
||||
const oldValues = {};
|
||||
const newValues = {};
|
||||
|
||||
const fieldMap = { item_name, item_type, description, warning_level, project_type, project_id };
|
||||
for (const [key, newVal] of Object.entries(fieldMap)) {
|
||||
if (newVal !== undefined && oldItem[key] !== newVal) {
|
||||
changedFields.push(key);
|
||||
oldValues[key] = oldItem[key];
|
||||
newValues[key] = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedFields.length > 0) {
|
||||
await db.query(`
|
||||
INSERT INTO zone_item_history (item_id, action_type, changed_fields, old_values, new_values, changed_by)
|
||||
VALUES (?, 'updated', ?, ?, ?, ?)
|
||||
`, [itemId, JSON.stringify(changedFields), JSON.stringify(oldValues), JSON.stringify(newValues), userId]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '현황이 수정되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('구역 현황 수정 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구역 현황 사진 업로드
|
||||
uploadZoneItemPhoto: async (req, res) => {
|
||||
try {
|
||||
const { item_id } = req.body;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, message: '파일이 없습니다.' });
|
||||
}
|
||||
|
||||
// 사진 테이블 생성 (없으면)
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS zone_item_photos (
|
||||
photo_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
item_id INT NOT NULL,
|
||||
photo_url VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_item_id (item_id)
|
||||
)
|
||||
`);
|
||||
|
||||
const photoUrl = `/uploads/${req.file.filename}`;
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO zone_item_photos (item_id, photo_url) VALUES (?, ?)`,
|
||||
[item_id, photoUrl]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { photo_id: result.insertId, photo_url: photoUrl },
|
||||
message: '사진이 업로드되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 업로드 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구역 현황 삭제
|
||||
deleteZoneItem: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const userId = req.user?.user_id;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 데이터 조회 (이력용)
|
||||
const [oldData] = await db.query(`SELECT * FROM workplace_zone_items WHERE item_id = ?`, [itemId]);
|
||||
const oldItem = oldData[0];
|
||||
|
||||
// 소프트 삭제
|
||||
await db.query(`UPDATE workplace_zone_items SET is_active = FALSE WHERE item_id = ?`, [itemId]);
|
||||
|
||||
// 삭제 이력 저장
|
||||
if (oldItem) {
|
||||
await db.query(`
|
||||
INSERT INTO zone_item_history (item_id, action_type, old_values, changed_by)
|
||||
VALUES (?, 'deleted', ?, ?)
|
||||
`, [itemId, JSON.stringify({ item_name: oldItem.item_name, item_type: oldItem.item_type }), userId]);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '현황이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('구역 현황 삭제 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구역 현황 이력 조회
|
||||
getZoneItemHistory: async (req, res) => {
|
||||
try {
|
||||
const { itemId } = req.params;
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const [history] = await db.query(`
|
||||
SELECT h.*, u.full_name as changed_by_name
|
||||
FROM zone_item_history h
|
||||
LEFT JOIN users u ON h.changed_by = u.user_id
|
||||
WHERE h.item_id = ?
|
||||
ORDER BY h.changed_at DESC
|
||||
LIMIT 50
|
||||
`, [itemId]);
|
||||
|
||||
res.json({ success: true, data: history });
|
||||
} catch (error) {
|
||||
console.error('현황 이력 조회 오류:', error);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PatrolController;
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 프로젝트 관리 컨트롤러
|
||||
*
|
||||
* 프로젝트 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const projectModel = require('../models/projectModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
const cache = require('../utils/cache');
|
||||
|
||||
/**
|
||||
* 프로젝트 생성
|
||||
*/
|
||||
exports.createProject = asyncHandler(async (req, res) => {
|
||||
const projectData = req.body;
|
||||
|
||||
logger.info('프로젝트 생성 요청', { name: projectData.name });
|
||||
|
||||
const id = await projectModel.create(projectData);
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 생성 성공', { project_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { project_id: id },
|
||||
message: '프로젝트가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 프로젝트 조회
|
||||
*/
|
||||
exports.getAllProjects = asyncHandler(async (req, res) => {
|
||||
const rows = await projectModel.getAll();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '프로젝트 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 활성 프로젝트만 조회 (작업보고서용)
|
||||
*/
|
||||
exports.getActiveProjects = asyncHandler(async (req, res) => {
|
||||
const rows = await projectModel.getActiveProjects();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '활성 프로젝트 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 프로젝트 조회
|
||||
*/
|
||||
exports.getProjectById = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 프로젝트 ID입니다');
|
||||
}
|
||||
|
||||
const row = await projectModel.getById(id);
|
||||
|
||||
if (!row) {
|
||||
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: row,
|
||||
message: '프로젝트 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 프로젝트 수정
|
||||
*/
|
||||
exports.updateProject = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 프로젝트 ID입니다');
|
||||
}
|
||||
|
||||
const data = { ...req.body, project_id: id };
|
||||
|
||||
const changes = await projectModel.update(data);
|
||||
|
||||
if (changes === 0) {
|
||||
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 수정 성공', { project_id: id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { changes },
|
||||
message: '프로젝트 정보가 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 프로젝트 삭제
|
||||
*/
|
||||
exports.removeProject = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 프로젝트 ID입니다');
|
||||
}
|
||||
|
||||
const changes = await projectModel.remove(id);
|
||||
|
||||
if (changes === 0) {
|
||||
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 프로젝트 캐시 무효화
|
||||
await cache.invalidateCache.project();
|
||||
|
||||
logger.info('프로젝트 삭제 성공', { project_id: id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '프로젝트가 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
@@ -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: '비밀번호 재설정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
152
deploy/tkfb-package/api.hyungi.net/controllers/taskController.js
Normal file
152
deploy/tkfb-package/api.hyungi.net/controllers/taskController.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 작업 관리 컨트롤러
|
||||
*
|
||||
* 작업 CRUD API 엔드포인트 핸들러
|
||||
* (공정=work_types에 속하는 세부 작업)
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-26
|
||||
*/
|
||||
|
||||
const taskModel = require('../models/taskModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// ==================== 작업 CRUD ====================
|
||||
|
||||
/**
|
||||
* 작업 생성
|
||||
*/
|
||||
exports.createTask = asyncHandler(async (req, res) => {
|
||||
const taskData = req.body;
|
||||
|
||||
if (!taskData.task_name) {
|
||||
throw new ValidationError('작업명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업 생성 요청', { name: taskData.task_name });
|
||||
|
||||
const id = await taskModel.createTask(taskData);
|
||||
|
||||
logger.info('작업 생성 성공', { task_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { task_id: id },
|
||||
message: '작업이 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 작업 조회 (work_type_id 필터 지원)
|
||||
*/
|
||||
exports.getAllTasks = asyncHandler(async (req, res) => {
|
||||
const { work_type_id } = req.query;
|
||||
|
||||
let rows;
|
||||
if (work_type_id) {
|
||||
// 특정 공정의 활성 작업만 조회
|
||||
rows = await taskModel.getTasksByWorkType(work_type_id);
|
||||
} else {
|
||||
rows = await taskModel.getAllTasks();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 활성 작업만 조회
|
||||
*/
|
||||
exports.getActiveTasks = asyncHandler(async (req, res) => {
|
||||
const rows = await taskModel.getActiveTasks();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '활성 작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 공정별 작업 조회
|
||||
*/
|
||||
exports.getTasksByWorkType = asyncHandler(async (req, res) => {
|
||||
const workTypeId = req.params.work_type_id || req.query.work_type_id;
|
||||
|
||||
if (!workTypeId) {
|
||||
throw new ValidationError('공정 ID가 필요합니다');
|
||||
}
|
||||
|
||||
const rows = await taskModel.getTasksByWorkType(workTypeId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '공정별 작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 작업 조회
|
||||
*/
|
||||
exports.getTaskById = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
|
||||
const task = await taskModel.getTaskById(taskId);
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundError('작업을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: task,
|
||||
message: '작업 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 수정
|
||||
*/
|
||||
exports.updateTask = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
const taskData = req.body;
|
||||
|
||||
if (!taskData.task_name) {
|
||||
throw new ValidationError('작업명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업 수정 요청', { task_id: taskId });
|
||||
|
||||
await taskModel.updateTask(taskId, taskData);
|
||||
|
||||
logger.info('작업 수정 성공', { task_id: taskId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업이 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 삭제
|
||||
*/
|
||||
exports.deleteTask = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
|
||||
logger.info('작업 삭제 요청', { task_id: taskId });
|
||||
|
||||
await taskModel.deleteTask(taskId);
|
||||
|
||||
logger.info('작업 삭제 성공', { task_id: taskId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업이 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
893
deploy/tkfb-package/api.hyungi.net/controllers/tbmController.js
Normal file
893
deploy/tkfb-package/api.hyungi.net/controllers/tbmController.js
Normal file
@@ -0,0 +1,893 @@
|
||||
// controllers/tbmController.js - TBM 시스템 컨트롤러
|
||||
const TbmModel = require('../models/tbmModel');
|
||||
|
||||
const TbmController = {
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
createSession: (req, res) => {
|
||||
const sessionData = {
|
||||
session_date: req.body.session_date,
|
||||
leader_id: req.body.leader_id || null,
|
||||
project_id: req.body.project_id || null,
|
||||
work_location: req.body.work_location || null,
|
||||
work_description: req.body.work_description || null,
|
||||
safety_notes: req.body.safety_notes || null,
|
||||
start_time: req.body.start_time || null,
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
// 필수 필드 검증 (날짜만 필수, leader_id는 관리자의 경우 null 허용)
|
||||
if (!sessionData.session_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'TBM 날짜는 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.createSession(sessionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 생성되었습니다.',
|
||||
data: {
|
||||
session_id: result.insertId,
|
||||
...sessionData
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 목록 조회
|
||||
*/
|
||||
getSessionsByDate: (req, res) => {
|
||||
const { date } = req.params;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜 정보가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getSessionsByDate(date, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 상세 조회
|
||||
*/
|
||||
getSessionById: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getSessionById(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 상세 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 상세 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results[0]
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 수정
|
||||
*/
|
||||
updateSession: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const sessionData = {
|
||||
project_id: req.body.project_id,
|
||||
work_location: req.body.work_location,
|
||||
work_description: req.body.work_description,
|
||||
safety_notes: req.body.safety_notes,
|
||||
status: req.body.status || 'draft'
|
||||
};
|
||||
|
||||
TbmModel.updateSession(sessionId, sessionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
completeSession: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const endTime = req.body.end_time || new Date().toTimeString().slice(0, 8);
|
||||
|
||||
TbmModel.completeSession(sessionId, endTime, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 완료 처리 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 완료 처리 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 완료되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
/**
|
||||
* 팀원 추가 (작업자별 상세 정보 포함)
|
||||
*/
|
||||
addTeamMember: (req, res) => {
|
||||
const assignmentData = {
|
||||
session_id: req.params.sessionId,
|
||||
worker_id: req.body.worker_id,
|
||||
assigned_role: req.body.assigned_role || null,
|
||||
work_detail: req.body.work_detail || null,
|
||||
is_present: req.body.is_present,
|
||||
absence_reason: req.body.absence_reason || null,
|
||||
project_id: req.body.project_id || null,
|
||||
work_type_id: req.body.work_type_id || null,
|
||||
task_id: req.body.task_id || null,
|
||||
workplace_category_id: req.body.workplace_category_id || null,
|
||||
workplace_id: req.body.workplace_id || null
|
||||
};
|
||||
|
||||
if (!assignmentData.worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.addTeamMember(assignmentData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '팀원이 추가되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀 구성 일괄 추가
|
||||
*/
|
||||
addTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { members } = req.body;
|
||||
|
||||
if (!Array.isArray(members) || members.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '팀원 목록이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.addTeamMembers(sessionId, members, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀 구성 일괄 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀 구성 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${members.length}명의 팀원이 추가되었습니다.`,
|
||||
data: { count: members.length }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 팀 구성 조회
|
||||
*/
|
||||
getTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getTeamMembers(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('팀 구성 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀 구성 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀원 제거
|
||||
*/
|
||||
removeTeamMember: (req, res) => {
|
||||
const { sessionId, workerId } = req.params;
|
||||
|
||||
TbmModel.removeTeamMember(sessionId, workerId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 제거 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 제거 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '팀원을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '팀원이 제거되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 세션의 모든 팀원 삭제 (수정 시 사용)
|
||||
*/
|
||||
clearAllTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.clearAllTeamMembers(sessionId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 전체 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '모든 팀원이 삭제되었습니다.',
|
||||
data: { deletedCount: result.affectedRows }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
/**
|
||||
* 모든 안전 체크 항목 조회
|
||||
*/
|
||||
getAllSafetyChecks: (req, res) => {
|
||||
TbmModel.getAllSafetyChecks((err, results) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 항목 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 안전 체크 기록 조회
|
||||
*/
|
||||
getSafetyRecords: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getSafetyRecords(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 기록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 기록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 일괄 저장
|
||||
*/
|
||||
saveSafetyRecords: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { records } = req.body;
|
||||
|
||||
if (!Array.isArray(records) || records.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '안전 체크 기록이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const checkedBy = req.user.user_id;
|
||||
|
||||
TbmModel.saveSafetyRecords(sessionId, records, checkedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 저장 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 저장 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전 체크가 저장되었습니다.',
|
||||
data: { count: records.length }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 필터링된 안전 체크리스트 (확장) ====================
|
||||
|
||||
/**
|
||||
* 세션에 맞는 필터링된 안전 체크 항목 조회
|
||||
* 기본 + 날씨 + 작업별 체크항목 통합
|
||||
*/
|
||||
getFilteredSafetyChecks: async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
try {
|
||||
// 날씨 정보 확인 (이미 저장된 경우 사용, 없으면 새로 조회)
|
||||
const weatherService = require('../services/weatherService');
|
||||
let weatherRecord = await weatherService.getWeatherRecord(sessionId);
|
||||
let weatherConditions = [];
|
||||
|
||||
if (weatherRecord && weatherRecord.weather_conditions) {
|
||||
weatherConditions = weatherRecord.weather_conditions;
|
||||
} else {
|
||||
// 날씨 정보가 없으면 현재 날씨 조회
|
||||
const currentWeather = await weatherService.getCurrentWeather();
|
||||
weatherConditions = await weatherService.determineWeatherConditions(currentWeather);
|
||||
// 날씨 기록 저장
|
||||
await weatherService.saveWeatherRecord(sessionId, currentWeather, weatherConditions);
|
||||
}
|
||||
|
||||
TbmModel.getFilteredSafetyChecks(sessionId, weatherConditions, (err, results) => {
|
||||
if (err) {
|
||||
console.error('필터링된 안전 체크 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크리스트 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('필터링된 안전 체크 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크리스트 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 현재 날씨 조회
|
||||
*/
|
||||
getCurrentWeather: async (req, res) => {
|
||||
try {
|
||||
const weatherService = require('../services/weatherService');
|
||||
const { nx, ny } = req.query;
|
||||
|
||||
const weatherData = await weatherService.getCurrentWeather(nx, ny);
|
||||
const conditions = await weatherService.determineWeatherConditions(weatherData);
|
||||
const conditionList = await weatherService.getWeatherConditionList();
|
||||
|
||||
// 현재 조건의 상세 정보 매핑
|
||||
const activeConditions = conditionList.filter(c => conditions.includes(c.condition_code));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...weatherData,
|
||||
conditions,
|
||||
conditionDetails: activeConditions
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('날씨 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '날씨 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 세션 날씨 정보 저장
|
||||
*/
|
||||
saveSessionWeather: async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { weatherConditions } = req.body;
|
||||
|
||||
try {
|
||||
const weatherService = require('../services/weatherService');
|
||||
|
||||
// 현재 날씨 조회
|
||||
const weatherData = await weatherService.getCurrentWeather();
|
||||
const conditions = weatherConditions || await weatherService.determineWeatherConditions(weatherData);
|
||||
|
||||
// 저장
|
||||
await weatherService.saveWeatherRecord(sessionId, weatherData, conditions);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '날씨 정보가 저장되었습니다.',
|
||||
data: { conditions }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('날씨 저장 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '날씨 저장 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 세션 날씨 정보 조회
|
||||
*/
|
||||
getSessionWeather: async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
try {
|
||||
const weatherService = require('../services/weatherService');
|
||||
const weatherRecord = await weatherService.getWeatherRecord(sessionId);
|
||||
|
||||
if (!weatherRecord) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '날씨 기록이 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weatherRecord
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('날씨 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '날씨 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 날씨 조건 목록 조회
|
||||
*/
|
||||
getWeatherConditions: async (req, res) => {
|
||||
try {
|
||||
const weatherService = require('../services/weatherService');
|
||||
const conditions = await weatherService.getWeatherConditionList();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: conditions
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('날씨 조건 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '날씨 조건 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 안전 체크항목 관리 (관리자용) ====================
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 생성
|
||||
*/
|
||||
createSafetyCheck: (req, res) => {
|
||||
const checkData = req.body;
|
||||
|
||||
if (!checkData.check_category || !checkData.check_item) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '카테고리와 체크 항목은 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.createSafetyCheck(checkData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 항목 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '안전 체크 항목이 생성되었습니다.',
|
||||
data: { check_id: result.insertId }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 수정
|
||||
*/
|
||||
updateSafetyCheck: (req, res) => {
|
||||
const { checkId } = req.params;
|
||||
const checkData = req.body;
|
||||
|
||||
TbmModel.updateSafetyCheck(checkId, checkData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 항목 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전 체크 항목이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 삭제 (비활성화)
|
||||
*/
|
||||
deleteSafetyCheck: (req, res) => {
|
||||
const { checkId } = req.params;
|
||||
|
||||
TbmModel.deleteSafetyCheck(checkId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전 체크 항목이 삭제되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업 인계 생성
|
||||
*/
|
||||
createHandover: (req, res) => {
|
||||
const handoverData = {
|
||||
session_id: req.body.session_id,
|
||||
from_leader_id: req.body.from_leader_id,
|
||||
to_leader_id: req.body.to_leader_id,
|
||||
handover_date: req.body.handover_date,
|
||||
handover_time: req.body.handover_time || null,
|
||||
reason: req.body.reason,
|
||||
handover_notes: req.body.handover_notes || null,
|
||||
worker_ids: req.body.worker_ids || []
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!handoverData.session_id || !handoverData.from_leader_id ||
|
||||
!handoverData.to_leader_id || !handoverData.handover_date || !handoverData.reason) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 정보가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.createHandover(handoverData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '작업 인계가 생성되었습니다.',
|
||||
data: { handover_id: result.insertId }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업 인계 확인
|
||||
*/
|
||||
confirmHandover: (req, res) => {
|
||||
const { handoverId } = req.params;
|
||||
const confirmedBy = req.user.user_id;
|
||||
|
||||
TbmModel.confirmHandover(handoverId, confirmedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 확인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 확인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '작업 인계 건을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업 인계가 확인되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업 인계 목록 조회
|
||||
*/
|
||||
getHandoversByDate: (req, res) => {
|
||||
const { date } = req.params;
|
||||
|
||||
TbmModel.getHandoversByDate(date, (err, results) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 나에게 온 미확인 인계 건 조회
|
||||
*/
|
||||
getMyPendingHandovers: (req, res) => {
|
||||
// worker_id는 req.user에서 가져옴
|
||||
const toLeaderId = req.user.worker_id;
|
||||
|
||||
if (!toLeaderId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 정보를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getPendingHandovers(toLeaderId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('미확인 인계 건 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '미확인 인계 건 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
/**
|
||||
* TBM 통계 조회
|
||||
*/
|
||||
getTbmStatistics: (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '시작일과 종료일이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getTbmStatistics(startDate, endDate, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 통계 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 리더별 TBM 진행 현황 조회
|
||||
*/
|
||||
getLeaderStatistics: (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '시작일과 종료일이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getLeaderStatistics(startDate, endDate, (err, results) => {
|
||||
if (err) {
|
||||
console.error('리더 통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '리더 통계 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업보고서가 작성되지 않은 TBM 팀 배정 조회
|
||||
*/
|
||||
getIncompleteWorkReports: (req, res) => {
|
||||
const userId = req.user.user_id;
|
||||
const accessLevel = req.user.access_level;
|
||||
|
||||
// 관리자는 모든 TBM 조회, 일반 사용자는 본인이 작성한 것만 조회
|
||||
const filterUserId = (accessLevel === 'system' || accessLevel === 'admin') ? null : userId;
|
||||
|
||||
TbmModel.getIncompleteWorkReports(filterUserId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('미완료 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '미완료 작업보고서 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TbmController;
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 도구 관리 컨트롤러
|
||||
*
|
||||
* 도구(공구) 재고 및 위치 관리 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const toolsService = require('../services/toolsService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 전체 도구 조회
|
||||
*/
|
||||
exports.getAll = asyncHandler(async (req, res) => {
|
||||
const rows = await toolsService.getAllToolsService();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '도구 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 도구 조회
|
||||
*/
|
||||
exports.getById = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const row = await toolsService.getToolByIdService(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: row,
|
||||
message: '도구 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 도구 생성
|
||||
*/
|
||||
exports.create = asyncHandler(async (req, res) => {
|
||||
const result = await toolsService.createToolService(req.body);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '도구가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 도구 수정
|
||||
*/
|
||||
exports.update = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const result = await toolsService.updateToolService(id, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '도구 정보가 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 도구 삭제
|
||||
*/
|
||||
exports.delete = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
await toolsService.deleteToolService(id);
|
||||
|
||||
res.status(204).send();
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 문서 업로드 관리 컨트롤러
|
||||
*
|
||||
* 파일 업로드 및 문서 메타데이터 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const uploadService = require('../services/uploadService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 문서 업로드
|
||||
*/
|
||||
exports.createUpload = asyncHandler(async (req, res) => {
|
||||
const doc = req.body;
|
||||
const result = await uploadService.createUploadService(doc);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '문서가 성공적으로 업로드되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 업로드 문서 조회
|
||||
*/
|
||||
exports.getUploads = asyncHandler(async (req, res) => {
|
||||
const rows = await uploadService.getAllUploadsService();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '업로드 문서 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
739
deploy/tkfb-package/api.hyungi.net/controllers/userController.js
Normal file
739
deploy/tkfb-package/api.hyungi.net/controllers/userController.js
Normal file
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* 사용자 관리 컨트롤러
|
||||
*
|
||||
* 사용자 CRUD 및 상태 관리 기능을 제공하는 컨트롤러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { ValidationError, ForbiddenError, NotFoundError, ConflictError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 관리자 권한 확인 헬퍼 함수
|
||||
*/
|
||||
const checkAdminPermission = (user) => {
|
||||
if (!user || !['admin', 'system'].includes(user.access_level)) {
|
||||
throw new ForbiddenError('관리자 권한이 필요합니다');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 사용자 조회
|
||||
*/
|
||||
const getAllUsers = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
logger.info('사용자 목록 조회 요청', { requestedBy: req.user?.username });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.email,
|
||||
u.role_id,
|
||||
r.name as role,
|
||||
u._access_level_old as access_level,
|
||||
u.is_active,
|
||||
u.worker_id,
|
||||
w.worker_name,
|
||||
w.department_id,
|
||||
d.department_name,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
u.last_login_at as last_login
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN workers w ON u.worker_id = w.worker_id
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
ORDER BY u.created_at DESC
|
||||
`;
|
||||
|
||||
const [users] = await db.execute(query);
|
||||
|
||||
logger.info('사용자 목록 조회 성공', { count: users.length });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users,
|
||||
message: '사용자 목록 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('사용자 목록 조회 실패', { error: error.message });
|
||||
throw new DatabaseError('사용자 목록을 조회하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 특정 사용자 조회
|
||||
*/
|
||||
const getUserById = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 조회 요청', { userId: id });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
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 NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 조회 성공', { userId: id, username: users[0].username });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users[0],
|
||||
message: '사용자 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 조회 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('사용자를 조회하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
const createUser = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { username, name, email, phone, role, password } = req.body;
|
||||
|
||||
logger.info('사용자 생성 요청', { username, name, role });
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !name || !role || !password) {
|
||||
throw new ValidationError('필수 필드가 누락되었습니다', {
|
||||
required: ['username', 'name', 'role', 'password'],
|
||||
received: { username, name, role, password: '***' }
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자명 유효성 검증
|
||||
if (username.length < 3 || username.length > 20) {
|
||||
throw new ValidationError('사용자명은 3-20자 사이여야 합니다');
|
||||
}
|
||||
|
||||
// 비밀번호 유효성 검증
|
||||
if (password.length < 6) {
|
||||
throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다');
|
||||
}
|
||||
|
||||
// 권한 레벨 검증
|
||||
const validRoles = ['admin', 'group_leader', 'worker'];
|
||||
if (!validRoles.includes(role)) {
|
||||
throw new ValidationError('유효하지 않은 권한입니다', {
|
||||
valid: validRoles,
|
||||
received: role
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자명 중복 확인
|
||||
const checkQuery = 'SELECT user_id FROM users WHERE username = ?';
|
||||
const [existing] = await db.execute(checkQuery, [username]);
|
||||
|
||||
if (existing.length > 0) {
|
||||
throw new ConflictError('이미 존재하는 사용자명입니다');
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
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
|
||||
]);
|
||||
|
||||
logger.info('사용자 생성 성공', {
|
||||
userId: result.insertId,
|
||||
username,
|
||||
name,
|
||||
role,
|
||||
createdBy: req.user.username
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { user_id: result.insertId },
|
||||
message: '사용자가 성공적으로 생성되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ConflictError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 생성 실패', { username, error: error.message });
|
||||
throw new DatabaseError('사용자를 생성하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
const updateUser = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
const { username, name, email, role, role_id, password, worker_id } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 수정 요청', { userId: id, body: req.body });
|
||||
|
||||
// 최소 하나의 수정 필드가 필요
|
||||
if (!username && !name && email === undefined && !role && !role_id && !password && worker_id === undefined) {
|
||||
throw new ValidationError('수정할 필드가 없습니다');
|
||||
}
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자 존재 확인
|
||||
const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?';
|
||||
const [existing] = await db.execute(checkQuery, [id]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
if (existing[0].is_active === 0) {
|
||||
throw new ValidationError('비활성화된 사용자는 수정할 수 없습니다');
|
||||
}
|
||||
|
||||
// 업데이트할 필드들
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (username) {
|
||||
if (username.length < 3 || username.length > 20) {
|
||||
throw new ValidationError('사용자명은 3-20자 사이여야 합니다');
|
||||
}
|
||||
|
||||
// 사용자명 중복 확인 (자신 제외)
|
||||
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 ConflictError('이미 존재하는 사용자명입니다');
|
||||
}
|
||||
|
||||
updates.push('username = ?');
|
||||
values.push(username);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
updates.push('name = ?');
|
||||
values.push(name);
|
||||
}
|
||||
|
||||
if (email !== undefined) {
|
||||
updates.push('email = ?');
|
||||
values.push(email || null);
|
||||
}
|
||||
|
||||
// role_id 또는 role 문자열 처리
|
||||
if (role_id) {
|
||||
// role_id가 유효한지 확인
|
||||
const [roleCheck] = await db.execute('SELECT id, name FROM roles WHERE id = ?', [role_id]);
|
||||
if (roleCheck.length === 0) {
|
||||
throw new ValidationError('유효하지 않은 역할 ID입니다');
|
||||
}
|
||||
updates.push('role_id = ?');
|
||||
values.push(role_id);
|
||||
logger.info('role_id로 역할 변경', { userId: id, role_id, role_name: roleCheck[0].name });
|
||||
} else if (role) {
|
||||
// role 문자열을 role_id로 변환 (하위 호환성)
|
||||
const roleNameMap = {
|
||||
'admin': 'Admin',
|
||||
'system': 'System Admin',
|
||||
'user': 'User',
|
||||
'guest': 'Guest',
|
||||
'group_leader': 'User', // 임시 매핑
|
||||
'worker': 'User' // 임시 매핑
|
||||
};
|
||||
const roleName = roleNameMap[role.toLowerCase()] || role;
|
||||
const [roleCheck] = await db.execute('SELECT id FROM roles WHERE name = ?', [roleName]);
|
||||
|
||||
if (roleCheck.length === 0) {
|
||||
throw new ValidationError(`유효하지 않은 권한입니다: ${role}`);
|
||||
}
|
||||
updates.push('role_id = ?');
|
||||
values.push(roleCheck[0].id);
|
||||
logger.info('role 문자열로 역할 변경', { userId: id, role, role_id: roleCheck[0].id });
|
||||
}
|
||||
|
||||
if (password) {
|
||||
if (password.length < 6) {
|
||||
throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다');
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
updates.push('password = ?');
|
||||
values.push(hashedPassword);
|
||||
}
|
||||
|
||||
// worker_id 업데이트 (null도 허용 - 연결 해제)
|
||||
if (worker_id !== undefined) {
|
||||
if (worker_id !== null) {
|
||||
// worker_id가 유효한지 확인
|
||||
const [workerCheck] = await db.execute('SELECT worker_id, worker_name FROM workers WHERE worker_id = ?', [worker_id]);
|
||||
if (workerCheck.length === 0) {
|
||||
throw new ValidationError('유효하지 않은 작업자 ID입니다');
|
||||
}
|
||||
logger.info('작업자 연결', { userId: id, worker_id, worker_name: workerCheck[0].worker_name });
|
||||
} else {
|
||||
logger.info('작업자 연결 해제', { userId: id });
|
||||
}
|
||||
updates.push('worker_id = ?');
|
||||
values.push(worker_id);
|
||||
}
|
||||
|
||||
updates.push('updated_at = NOW()');
|
||||
values.push(id);
|
||||
|
||||
const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`;
|
||||
|
||||
logger.info('실행할 UPDATE 쿼리', { query: updateQuery, values });
|
||||
await db.execute(updateQuery, values);
|
||||
|
||||
logger.info('사용자 수정 성공', {
|
||||
userId: id,
|
||||
username: existing[0].username,
|
||||
updatedFields: Object.keys(req.body),
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id },
|
||||
message: '사용자 정보가 성공적으로 수정되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError || error instanceof ConflictError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 수정 실패', { userId: id, error: error.message, stack: error.stack });
|
||||
throw new DatabaseError('사용자 정보를 수정하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 상태 변경 (활성화/비활성화)
|
||||
*/
|
||||
const updateUserStatus = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
const { is_active } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
if (is_active === undefined || ![0, 1, true, false].includes(is_active)) {
|
||||
throw new ValidationError('유효하지 않은 활성 상태 값입니다');
|
||||
}
|
||||
|
||||
const activeValue = is_active === true || is_active === 1 ? 1 : 0;
|
||||
|
||||
// 자기 자신 비활성화 방지
|
||||
if (parseInt(id) === req.user.user_id && activeValue === 0) {
|
||||
throw new ValidationError('자기 자신을 비활성화할 수 없습니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 상태 변경 요청', { userId: id, is_active: activeValue });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자 존재 확인
|
||||
const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?';
|
||||
const [users] = await db.execute(checkQuery, [id]);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 상태 변경이 필요한지 확인
|
||||
if (users[0].is_active === activeValue) {
|
||||
const status = activeValue === 1 ? '활성' : '비활성';
|
||||
throw new ValidationError(`사용자가 이미 ${status} 상태입니다`);
|
||||
}
|
||||
|
||||
const query = 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE user_id = ?';
|
||||
await db.execute(query, [activeValue, id]);
|
||||
|
||||
const statusText = activeValue === 1 ? '활성화' : '비활성화';
|
||||
|
||||
logger.info(`사용자 ${statusText} 성공`, {
|
||||
userId: id,
|
||||
username: users[0].username,
|
||||
newStatus: activeValue,
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id, is_active: activeValue },
|
||||
message: `사용자가 성공적으로 ${statusText}되었습니다`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 상태 변경 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('사용자 상태를 변경하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 삭제 (Soft Delete)
|
||||
*/
|
||||
const deleteUser = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (req.user && req.user.user_id == id) {
|
||||
throw new ValidationError('자기 자신은 삭제할 수 없습니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 삭제 요청', { userId: id });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자 존재 확인
|
||||
const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?';
|
||||
const [users] = await db.execute(checkQuery, [id]);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
if (users[0].is_active === 0) {
|
||||
throw new ValidationError('이미 비활성화된 사용자입니다');
|
||||
}
|
||||
|
||||
// Soft Delete (is_active = 0)
|
||||
const query = 'UPDATE users SET is_active = 0, updated_at = NOW() WHERE user_id = ?';
|
||||
await db.execute(query, [id]);
|
||||
|
||||
logger.info('사용자 비활성화 성공', {
|
||||
userId: id,
|
||||
username: users[0].username,
|
||||
deletedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id },
|
||||
message: '사용자가 성공적으로 비활성화되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 비활성화 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('사용자를 비활성화하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 영구 삭제 (Hard Delete)
|
||||
*/
|
||||
const permanentDeleteUser = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (req.user && req.user.user_id == id) {
|
||||
throw new ValidationError('자기 자신은 삭제할 수 없습니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 영구 삭제 요청', { userId: id });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자 존재 확인
|
||||
const checkQuery = 'SELECT user_id, username FROM users WHERE user_id = ?';
|
||||
const [users] = await db.execute(checkQuery, [id]);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
const username = users[0].username;
|
||||
|
||||
// 관련 데이터 삭제 (외래 키 제약 조건 때문에 순서 중요)
|
||||
// 1. 로그인 로그 삭제
|
||||
await db.execute('DELETE FROM login_logs WHERE user_id = ?', [id]);
|
||||
|
||||
// 2. 페이지 접근 권한 삭제
|
||||
await db.execute('DELETE FROM user_page_access WHERE user_id = ?', [id]);
|
||||
|
||||
// 3. 사용자 삭제
|
||||
await db.execute('DELETE FROM users WHERE user_id = ?', [id]);
|
||||
|
||||
logger.info('사용자 영구 삭제 성공', {
|
||||
userId: id,
|
||||
username: username,
|
||||
deletedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id },
|
||||
message: `사용자 "${username}"이(가) 영구적으로 삭제되었습니다`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 영구 삭제 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('사용자를 영구 삭제하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한 조회
|
||||
*/
|
||||
const getUserPageAccess = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 페이지 권한 조회 요청', { userId: id });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 권한 조회: user_page_access에 명시적 권한이 있으면 사용, 없으면 is_default_accessible 사용
|
||||
const query = `
|
||||
SELECT
|
||||
p.id as page_id,
|
||||
p.page_key,
|
||||
p.page_name,
|
||||
p.page_path,
|
||||
p.category,
|
||||
p.is_default_accessible,
|
||||
COALESCE(upa.can_access, p.is_default_accessible) as can_access
|
||||
FROM pages p
|
||||
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
|
||||
ORDER BY p.category, p.display_order
|
||||
`;
|
||||
|
||||
const [pageAccess] = await db.execute(query, [id]);
|
||||
|
||||
logger.info('사용자 페이지 권한 조회 성공', { userId: id, pageCount: pageAccess.length });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
pageAccess
|
||||
},
|
||||
message: '페이지 권한 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('사용자 페이지 권한 조회 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('페이지 권한을 조회하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한 업데이트
|
||||
*/
|
||||
const updateUserPageAccess = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
const { pageAccess } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
if (!Array.isArray(pageAccess)) {
|
||||
throw new ValidationError('pageAccess는 배열이어야 합니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 페이지 권한 업데이트 요청', {
|
||||
userId: id,
|
||||
pageCount: pageAccess.length,
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 트랜잭션 시작
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
// 기존 권한 삭제
|
||||
await db.execute('DELETE FROM user_page_access WHERE user_id = ?', [id]);
|
||||
|
||||
// 새 권한 삽입
|
||||
if (pageAccess.length > 0) {
|
||||
const values = pageAccess.map(p => [id, p.page_id, p.can_access]);
|
||||
const placeholders = values.map(() => '(?, ?, ?)').join(', ');
|
||||
const flatValues = values.flat();
|
||||
|
||||
await db.execute(
|
||||
`INSERT INTO user_page_access (user_id, page_id, can_access) VALUES ${placeholders}`,
|
||||
flatValues
|
||||
);
|
||||
}
|
||||
|
||||
// 커밋
|
||||
await db.query('COMMIT');
|
||||
|
||||
logger.info('사용자 페이지 권한 업데이트 성공', {
|
||||
userId: id,
|
||||
pageCount: pageAccess.length,
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id },
|
||||
message: '페이지 권한이 성공적으로 업데이트되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
// 롤백
|
||||
await db.query('ROLLBACK');
|
||||
logger.error('사용자 페이지 권한 업데이트 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('페이지 권한을 업데이트하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 비밀번호 초기화 (000000)
|
||||
*/
|
||||
const resetUserPassword = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await db.execute('SELECT user_id, username FROM users WHERE user_id = ?', [id]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 비밀번호를 000000으로 초기화
|
||||
const hashedPassword = await bcrypt.hash('000000', 10);
|
||||
await db.execute(
|
||||
'UPDATE users SET password = ?, password_changed_at = NULL, updated_at = NOW() WHERE user_id = ?',
|
||||
[hashedPassword, id]
|
||||
);
|
||||
|
||||
logger.info('사용자 비밀번호 초기화 성공', {
|
||||
userId: id,
|
||||
username: existing[0].username,
|
||||
resetBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '비밀번호가 000000으로 초기화되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('비밀번호 초기화 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('비밀번호 초기화에 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserStatus,
|
||||
deleteUser,
|
||||
permanentDeleteUser,
|
||||
getUserPageAccess,
|
||||
updateUserPageAccess,
|
||||
resetUserPassword
|
||||
};
|
||||
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* vacationBalanceController.js
|
||||
* 휴가 잔액 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationBalanceModel = require('../models/vacationBalanceModel');
|
||||
const vacationTypeModel = require('../models/vacationTypeModel');
|
||||
|
||||
const vacationBalanceController = {
|
||||
/**
|
||||
* 특정 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* GET /api/vacation-balances/worker/:workerId/year/:year
|
||||
*/
|
||||
async getByWorkerAndYear(req, res) {
|
||||
try {
|
||||
const { workerId, year } = req.params;
|
||||
|
||||
vacationBalanceModel.getByWorkerAndYear(workerId, year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getByWorkerAndYear 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* GET /api/vacation-balances/year/:year
|
||||
*/
|
||||
async getAllByYear(req, res) {
|
||||
try {
|
||||
const { year } = req.params;
|
||||
|
||||
vacationBalanceModel.getAllByYear(year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('전체 휴가 잔액 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '전체 휴가 잔액을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAllByYear 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 생성
|
||||
* POST /api/vacation-balances
|
||||
*/
|
||||
async createBalance(req, res) {
|
||||
try {
|
||||
const {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
year,
|
||||
total_days,
|
||||
used_days,
|
||||
notes
|
||||
} = req.body;
|
||||
const created_by = req.user.user_id;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !vacation_type_id || !year || total_days === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (worker_id, vacation_type_id, year, total_days)'
|
||||
});
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
vacationBalanceModel.getByWorkerTypeYear(worker_id, vacation_type_id, year, (err, existing) => {
|
||||
if (err) {
|
||||
console.error('중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 해당 작업자의 해당 연도 휴가 잔액이 존재합니다'
|
||||
});
|
||||
}
|
||||
|
||||
const balanceData = {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
year,
|
||||
total_days,
|
||||
used_days: used_days || 0,
|
||||
notes: notes || null,
|
||||
created_by
|
||||
};
|
||||
|
||||
vacationBalanceModel.create(balanceData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 생성되었습니다',
|
||||
data: { id: result.insertId }
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('createBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 수정
|
||||
* PUT /api/vacation-balances/:id
|
||||
*/
|
||||
async updateBalance(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { total_days, used_days, notes } = req.body;
|
||||
|
||||
const updateData = {};
|
||||
if (total_days !== undefined) updateData.total_days = total_days;
|
||||
if (used_days !== undefined) updateData.used_days = used_days;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
updateData.updated_at = new Date();
|
||||
|
||||
if (Object.keys(updateData).length === 1) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수정할 데이터가 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationBalanceModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 수정하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updateBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 삭제
|
||||
* DELETE /api/vacation-balances/:id
|
||||
*/
|
||||
async deleteBalance(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationBalanceModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 삭제하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('deleteBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 근속년수 기반 연차 자동 계산 및 생성
|
||||
* POST /api/vacation-balances/auto-calculate
|
||||
*/
|
||||
async autoCalculateAndCreate(req, res) {
|
||||
try {
|
||||
const { worker_id, hire_date, year } = req.body;
|
||||
const created_by = req.user.user_id;
|
||||
|
||||
if (!worker_id || !hire_date || !year) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (worker_id, hire_date, year)'
|
||||
});
|
||||
}
|
||||
|
||||
// 연차 일수 계산
|
||||
const annualDays = vacationBalanceModel.calculateAnnualLeaveDays(hire_date, year);
|
||||
|
||||
// ANNUAL 휴가 유형 ID 조회
|
||||
vacationTypeModel.getByCode('ANNUAL', (err, types) => {
|
||||
if (err || !types || types.length === 0) {
|
||||
console.error('ANNUAL 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'ANNUAL 휴가 유형을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const annualTypeId = types[0].id;
|
||||
|
||||
// 중복 체크
|
||||
vacationBalanceModel.getByWorkerTypeYear(worker_id, annualTypeId, year, (err, existing) => {
|
||||
if (err) {
|
||||
console.error('중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 해당 작업자의 해당 연도 연차가 존재합니다'
|
||||
});
|
||||
}
|
||||
|
||||
const balanceData = {
|
||||
worker_id,
|
||||
vacation_type_id: annualTypeId,
|
||||
year,
|
||||
total_days: annualDays,
|
||||
used_days: 0,
|
||||
notes: `근속년수 기반 자동 계산 (입사일: ${hire_date})`,
|
||||
created_by
|
||||
};
|
||||
|
||||
vacationBalanceModel.create(balanceData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${annualDays}일의 연차가 자동으로 생성되었습니다`,
|
||||
data: {
|
||||
id: result.insertId,
|
||||
calculated_days: annualDays
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('autoCalculateAndCreate 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 일괄 저장 (upsert)
|
||||
* POST /api/vacation-balances/bulk-upsert
|
||||
*/
|
||||
async bulkUpsert(req, res) {
|
||||
try {
|
||||
const { balances } = req.body;
|
||||
const created_by = req.user.user_id;
|
||||
|
||||
if (!balances || !Array.isArray(balances) || balances.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '저장할 데이터가 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const balance of balances) {
|
||||
const { worker_id, vacation_type_id, year, total_days, notes } = balance;
|
||||
|
||||
if (!worker_id || !vacation_type_id || !year || total_days === undefined) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Upsert 쿼리
|
||||
const query = `
|
||||
INSERT INTO vacation_balance_details
|
||||
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES (?, ?, ?, ?, 0, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_days = VALUES(total_days),
|
||||
notes = VALUES(notes),
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await db.query(query, [worker_id, vacation_type_id, year, total_days, notes || null, created_by]);
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
console.error('휴가 잔액 저장 오류:', err);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${successCount}건 저장 완료${errorCount > 0 ? `, ${errorCount}건 실패` : ''}`,
|
||||
data: { successCount, errorCount }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('bulkUpsert 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 사용 가능한 휴가 일수 조회
|
||||
* GET /api/vacation-balances/worker/:workerId/year/:year/available
|
||||
*/
|
||||
async getAvailableDays(req, res) {
|
||||
try {
|
||||
const { workerId, year } = req.params;
|
||||
|
||||
vacationBalanceModel.getAvailableVacationDays(workerId, year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('사용 가능 휴가 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사용 가능 휴가를 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAvailableDays 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationBalanceController;
|
||||
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* vacationRequestController.js
|
||||
* 휴가 신청 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationRequestModel = require('../models/vacationRequestModel');
|
||||
// TODO: workerVacationBalanceModel 구현 필요
|
||||
// const workerVacationBalanceModel = require('../models/workerVacationBalanceModel');
|
||||
|
||||
const vacationRequestController = {
|
||||
/**
|
||||
* 휴가 신청 생성
|
||||
*/
|
||||
async createRequest(req, res) {
|
||||
try {
|
||||
const { worker_id, vacation_type_id, start_date, end_date, days_used, reason } = req.body;
|
||||
const requested_by = req.user.user_id;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !vacation_type_id || !start_date || !end_date || !days_used) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 유효성 검증
|
||||
const startDate = new Date(start_date);
|
||||
const endDate = new Date(end_date);
|
||||
|
||||
if (endDate < startDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '종료일은 시작일보다 이후여야 합니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기간 중복 체크
|
||||
vacationRequestModel.checkOverlap(worker_id, start_date, end_date, null, (err, results) => {
|
||||
if (err) {
|
||||
console.error('기간 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '기간 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results[0].count > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '해당 기간에 이미 신청된 휴가가 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 잔여 연차 확인 로직 구현 필요
|
||||
// 현재는 잔여 연차 확인 없이 신청 가능
|
||||
|
||||
// 휴가 신청 생성
|
||||
const requestData = {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
start_date,
|
||||
end_date,
|
||||
days_used,
|
||||
reason: reason || null,
|
||||
status: 'pending',
|
||||
requested_by
|
||||
};
|
||||
|
||||
vacationRequestModel.create(requestData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 생성 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '휴가 신청이 완료되었습니다',
|
||||
data: {
|
||||
request_id: result.insertId
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 목록 조회
|
||||
*/
|
||||
async getAllRequests(req, res) {
|
||||
try {
|
||||
const filters = {
|
||||
worker_id: req.query.worker_id,
|
||||
status: req.query.status,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
vacation_type_id: req.query.vacation_type_id
|
||||
};
|
||||
|
||||
// 일반 사용자는 자신의 신청만 조회 가능
|
||||
if (req.user.access_level !== 'system') {
|
||||
if (req.user.worker_id) {
|
||||
filters.worker_id = req.user.worker_id;
|
||||
} else {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vacationRequestModel.getAll(filters, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 목록 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 휴가 신청 조회
|
||||
*/
|
||||
async getRequestById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
// 권한 검증: 관리자 또는 본인만 조회 가능
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== request.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: request
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 수정 (대기 중인 신청만)
|
||||
*/
|
||||
async updateRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { start_date, end_date, days_used, reason } = req.body;
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const existingRequest = results[0];
|
||||
|
||||
// 권한 검증
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 대기 중인 신청만 수정 가능
|
||||
if (existingRequest.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '승인/거부된 신청은 수정할 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (start_date) updateData.start_date = start_date;
|
||||
if (end_date) updateData.end_date = end_date;
|
||||
if (days_used) updateData.days_used = days_used;
|
||||
if (reason !== undefined) updateData.reason = reason;
|
||||
|
||||
// 날짜가 변경된 경우 중복 체크
|
||||
if (start_date || end_date) {
|
||||
const newStartDate = start_date || existingRequest.start_date;
|
||||
const newEndDate = end_date || existingRequest.end_date;
|
||||
|
||||
vacationRequestModel.checkOverlap(
|
||||
existingRequest.worker_id,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
id,
|
||||
(err, overlapResults) => {
|
||||
if (err) {
|
||||
console.error('기간 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '기간 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (overlapResults[0].count > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '해당 기간에 이미 신청된 휴가가 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 수정 실행
|
||||
vacationRequestModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 수정 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 날짜 변경 없이 바로 수정
|
||||
vacationRequestModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 수정 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 삭제 (대기 중인 신청만)
|
||||
*/
|
||||
async deleteRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const existingRequest = results[0];
|
||||
|
||||
// 권한 검증
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 대기 중인 신청만 삭제 가능
|
||||
if (existingRequest.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '승인/거부된 신청은 삭제할 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationRequestModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 삭제 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 승인 (관리자만)
|
||||
*/
|
||||
async approveRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { review_note } = req.body;
|
||||
const reviewed_by = req.user.user_id;
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 승인할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
if (request.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 처리된 신청입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
const statusData = {
|
||||
status: 'approved',
|
||||
reviewed_by,
|
||||
review_note
|
||||
};
|
||||
|
||||
vacationRequestModel.updateStatus(id, statusData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 승인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 승인 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 잔여 연차에서 차감 로직 구현 필요
|
||||
// 현재는 연차 차감 없이 승인만 처리
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 승인되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 승인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 거부 (관리자만)
|
||||
*/
|
||||
async rejectRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { review_note } = req.body;
|
||||
const reviewed_by = req.user.user_id;
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 거부할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
if (request.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 처리된 신청입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
const statusData = {
|
||||
status: 'rejected',
|
||||
reviewed_by,
|
||||
review_note
|
||||
};
|
||||
|
||||
vacationRequestModel.updateStatus(id, statusData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 거부 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 거부 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 거부되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 거부 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 대기 중인 휴가 신청 목록 (관리자용)
|
||||
*/
|
||||
async getPendingRequests(req, res) {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 조회할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationRequestModel.getAllPending((err, results) => {
|
||||
if (err) {
|
||||
console.error('대기 중인 휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '대기 중인 휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('대기 중인 휴가 신청 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationRequestController;
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* vacationTypeController.js
|
||||
* 휴가 유형 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationTypeModel = require('../models/vacationTypeModel');
|
||||
|
||||
const vacationTypeController = {
|
||||
/**
|
||||
* 모든 활성 휴가 유형 조회
|
||||
* GET /api/vacation-types
|
||||
*/
|
||||
async getAllTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getAll((err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAllTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 시스템 기본 휴가 유형 조회
|
||||
* GET /api/vacation-types/system
|
||||
*/
|
||||
async getSystemTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getSystemTypes((err, results) => {
|
||||
if (err) {
|
||||
console.error('시스템 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '시스템 휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getSystemTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 조회
|
||||
* GET /api/vacation-types/special
|
||||
*/
|
||||
async getSpecialTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getSpecialTypes((err, results) => {
|
||||
if (err) {
|
||||
console.error('특별 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '특별 휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getSpecialTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 생성 (관리자만)
|
||||
* POST /api/vacation-types
|
||||
*/
|
||||
async createType(req, res) {
|
||||
try {
|
||||
const {
|
||||
type_code,
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority,
|
||||
description
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!type_code || !type_name || !deduct_days) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (type_code, type_name, deduct_days)'
|
||||
});
|
||||
}
|
||||
|
||||
// type_code 중복 체크
|
||||
vacationTypeModel.getByCode(type_code, (err, existingTypes) => {
|
||||
if (err) {
|
||||
console.error('type_code 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'type_code 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existingTypes && existingTypes.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 존재하는 type_code입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 특별 휴가 유형으로 생성
|
||||
const typeData = {
|
||||
type_code,
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority: priority || 50,
|
||||
description: description || null,
|
||||
is_special: true,
|
||||
is_system: false,
|
||||
is_active: true
|
||||
};
|
||||
|
||||
vacationTypeModel.create(typeData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '특별 휴가 유형이 생성되었습니다',
|
||||
data: { id: result.insertId }
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('createType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 수정 (관리자만)
|
||||
* PUT /api/vacation-types/:id
|
||||
*/
|
||||
async updateType(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority,
|
||||
description,
|
||||
is_active
|
||||
} = req.body;
|
||||
|
||||
// 먼저 해당 유형 조회
|
||||
vacationTypeModel.getById(id, (err, types) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (!types || types.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const type = types[0];
|
||||
|
||||
// 시스템 기본 휴가의 경우 제한적으로만 수정 가능
|
||||
const updateData = {};
|
||||
if (type.is_system) {
|
||||
// 시스템 휴가는 priority와 description만 수정 가능
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
} else {
|
||||
// 특별 휴가는 모든 필드 수정 가능
|
||||
if (type_name) updateData.type_name = type_name;
|
||||
if (deduct_days !== undefined) updateData.deduct_days = deduct_days;
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (is_active !== undefined) updateData.is_active = is_active;
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수정할 데이터가 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_at = new Date();
|
||||
|
||||
vacationTypeModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 수정하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 유형이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updateType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 삭제 (관리자만, 시스템 기본 휴가는 삭제 불가)
|
||||
* DELETE /api/vacation-types/:id
|
||||
*/
|
||||
async deleteType(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationTypeModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 삭제하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '삭제할 수 없습니다. 시스템 기본 휴가이거나 존재하지 않는 휴가 유형입니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 유형이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('deleteType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 우선순위 일괄 업데이트 (관리자만)
|
||||
* PUT /api/vacation-types/priorities
|
||||
*/
|
||||
async updatePriorities(req, res) {
|
||||
try {
|
||||
const { priorities } = req.body;
|
||||
|
||||
// priorities = [{ id: 1, priority: 10 }, { id: 2, priority: 20 }, ...]
|
||||
if (!priorities || !Array.isArray(priorities)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'priorities 배열이 필요합니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationTypeModel.updatePriorities(priorities, (err, result) => {
|
||||
if (err) {
|
||||
console.error('우선순위 업데이트 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '우선순위를 업데이트하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '우선순위가 업데이트되었습니다',
|
||||
data: { updated: result.affectedRows }
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updatePriorities 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationTypeController;
|
||||
@@ -0,0 +1,555 @@
|
||||
const visitRequestModel = require('../models/visitRequestModel');
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 생성
|
||||
*/
|
||||
exports.createVisitRequest = (req, res) => {
|
||||
const requester_id = req.user.user_id;
|
||||
const requestData = {
|
||||
requester_id,
|
||||
...req.body
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
|
||||
for (const field of requiredFields) {
|
||||
if (!requestData[field]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `${field}는 필수 입력 항목입니다.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visitRequestModel.createVisitRequest(requestData, (err, requestId) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '출입 신청이 성공적으로 생성되었습니다.',
|
||||
data: { request_id: requestId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 목록 조회
|
||||
*/
|
||||
exports.getAllVisitRequests = (req, res) => {
|
||||
const filters = {
|
||||
status: req.query.status,
|
||||
visit_date: req.query.visit_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
requester_id: req.query.requester_id,
|
||||
category_id: req.query.category_id
|
||||
};
|
||||
|
||||
visitRequestModel.getAllVisitRequests(filters, (err, requests) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: requests
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상세 조회
|
||||
*/
|
||||
exports.getVisitRequestById = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
|
||||
visitRequestModel.getVisitRequestById(requestId, (err, request) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: request
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 수정
|
||||
*/
|
||||
exports.updateVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const requestData = req.body;
|
||||
|
||||
visitRequestModel.updateVisitRequest(requestId, requestData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 삭제
|
||||
*/
|
||||
exports.deleteVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
|
||||
visitRequestModel.deleteVisitRequest(requestId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 삭제되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 승인
|
||||
*/
|
||||
exports.approveVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const approvedBy = req.user.user_id;
|
||||
|
||||
visitRequestModel.approveVisitRequest(requestId, approvedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 승인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 승인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 승인되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 반려
|
||||
*/
|
||||
exports.rejectVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const approvedBy = req.user.user_id;
|
||||
const rejectionReason = req.body.rejection_reason || '사유 없음';
|
||||
|
||||
const rejectionData = {
|
||||
approved_by: approvedBy,
|
||||
rejection_reason: rejectionReason
|
||||
};
|
||||
|
||||
visitRequestModel.rejectVisitRequest(requestId, rejectionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 반려 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 반려 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 반려되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 방문 목적 조회
|
||||
*/
|
||||
exports.getAllVisitPurposes = (req, res) => {
|
||||
visitRequestModel.getAllVisitPurposes((err, purposes) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: purposes
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 방문 목적만 조회
|
||||
*/
|
||||
exports.getActiveVisitPurposes = (req, res) => {
|
||||
visitRequestModel.getActiveVisitPurposes((err, purposes) => {
|
||||
if (err) {
|
||||
console.error('활성 방문 목적 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '활성 방문 목적 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: purposes
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 추가
|
||||
*/
|
||||
exports.createVisitPurpose = (req, res) => {
|
||||
const purposeData = req.body;
|
||||
|
||||
if (!purposeData.purpose_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'purpose_name은 필수 입력 항목입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.createVisitPurpose(purposeData, (err, purposeId) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '방문 목적이 추가되었습니다.',
|
||||
data: { purpose_id: purposeId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 수정
|
||||
*/
|
||||
exports.updateVisitPurpose = (req, res) => {
|
||||
const purposeId = req.params.id;
|
||||
const purposeData = req.body;
|
||||
|
||||
visitRequestModel.updateVisitPurpose(purposeId, purposeData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '방문 목적을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '방문 목적이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 삭제
|
||||
*/
|
||||
exports.deleteVisitPurpose = (req, res) => {
|
||||
const purposeId = req.params.id;
|
||||
|
||||
visitRequestModel.deleteVisitPurpose(purposeId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '방문 목적을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '방문 목적이 삭제되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
/**
|
||||
* 안전교육 기록 생성
|
||||
*/
|
||||
exports.createTrainingRecord = (req, res) => {
|
||||
const trainerId = req.user.user_id;
|
||||
const trainingData = {
|
||||
trainer_id: trainerId,
|
||||
...req.body
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = ['request_id', 'training_date', 'training_start_time'];
|
||||
for (const field of requiredFields) {
|
||||
if (!trainingData[field]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `${field}는 필수 입력 항목입니다.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visitRequestModel.createTrainingRecord(trainingData, (err, trainingId) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 안전교육 기록이 생성되면 출입 신청 상태를 training_completed로 변경
|
||||
console.log(`[교육 완료] request_id=${trainingData.request_id} 상태를 training_completed로 변경 중...`);
|
||||
visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed', (statusErr) => {
|
||||
if (statusErr) {
|
||||
console.error('출입 신청 상태 업데이트 오류:', statusErr);
|
||||
// 에러가 발생해도 교육 기록은 생성되었으므로 성공 응답
|
||||
} else {
|
||||
console.log(`[교육 완료] request_id=${trainingData.request_id} 상태 변경 성공`);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '안전교육 기록이 생성되었습니다.',
|
||||
data: { training_id: trainingId }
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 출입 신청의 안전교육 기록 조회
|
||||
*/
|
||||
exports.getTrainingRecordByRequestId = (req, res) => {
|
||||
const requestId = req.params.requestId;
|
||||
|
||||
visitRequestModel.getTrainingRecordByRequestId(requestId, (err, record) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record || null
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 수정
|
||||
*/
|
||||
exports.updateTrainingRecord = (req, res) => {
|
||||
const trainingId = req.params.id;
|
||||
const trainingData = req.body;
|
||||
|
||||
visitRequestModel.updateTrainingRecord(trainingId, trainingData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전교육 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전교육 기록이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 완료 (서명 포함)
|
||||
*/
|
||||
exports.completeTraining = (req, res) => {
|
||||
const trainingId = req.params.id;
|
||||
const signatureData = req.body.signature_data;
|
||||
|
||||
if (!signatureData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '서명 데이터가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.completeTraining(trainingId, signatureData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전교육 완료 처리 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 완료 처리 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전교육 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 교육 완료 후 출입 신청 상태를 'training_completed'로 변경
|
||||
visitRequestModel.getTrainingRecordByRequestId(trainingId, (err, record) => {
|
||||
if (err || !record) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '안전교육이 완료되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed', (err) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 상태 업데이트 오류:', err);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전교육이 완료되었습니다.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 목록 조회
|
||||
*/
|
||||
exports.getTrainingRecords = (req, res) => {
|
||||
const filters = {
|
||||
training_date: req.query.training_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
trainer_id: req.query.trainer_id
|
||||
};
|
||||
|
||||
visitRequestModel.getTrainingRecords(filters, (err, records) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: records
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* 작업 분석 컨트롤러
|
||||
*
|
||||
* 작업 보고서 다차원 분석 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const WorkAnalysis = require('../models/WorkAnalysis');
|
||||
const { getDb } = require('../dbPool');
|
||||
const { ValidationError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 날짜 유효성 검사 헬퍼 함수
|
||||
*/
|
||||
const validateDateRange = (startDate, endDate) => {
|
||||
if (!startDate || !endDate) {
|
||||
throw new ValidationError('시작일과 종료일을 입력해주세요', {
|
||||
required: ['start', 'end'],
|
||||
received: { start: startDate, end: endDate }
|
||||
});
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
throw new ValidationError('올바른 날짜 형식을 입력해주세요', {
|
||||
format: 'YYYY-MM-DD',
|
||||
received: { start: startDate, end: endDate }
|
||||
});
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
throw new ValidationError('시작일이 종료일보다 늦을 수 없습니다', {
|
||||
start: startDate,
|
||||
end: endDate
|
||||
});
|
||||
}
|
||||
|
||||
// 너무 긴 기간 방지 (1년 제한)
|
||||
const diffTime = Math.abs(end - start);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
if (diffDays > 365) {
|
||||
throw new ValidationError('조회 기간은 1년을 초과할 수 없습니다', {
|
||||
days: diffDays,
|
||||
max: 365
|
||||
});
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
/**
|
||||
* 기본 통계 조회
|
||||
*/
|
||||
const getStats = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('기본 통계 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const stats = await workAnalysis.getBasicStats(start, end);
|
||||
|
||||
logger.info('기본 통계 조회 성공', { start, end });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
message: '기본 통계 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('기본 통계 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('기본 통계 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 일별 작업시간 추이 조회
|
||||
*/
|
||||
const getDailyTrend = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('일별 추이 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const trendData = await workAnalysis.getDailyTrend(start, end);
|
||||
|
||||
logger.info('일별 추이 조회 성공', { start, end, dataPoints: trendData.length });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData,
|
||||
message: '일별 추이 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('일별 추이 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('일별 추이 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자별 통계 조회
|
||||
*/
|
||||
const getWorkerStats = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('작업자별 통계 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workerStats = await workAnalysis.getWorkerStats(start, end);
|
||||
|
||||
logger.info('작업자별 통계 조회 성공', {
|
||||
start,
|
||||
end,
|
||||
workerCount: workerStats.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: workerStats,
|
||||
message: '작업자별 통계 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('작업자별 통계 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('작업자별 통계 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 프로젝트별 통계 조회
|
||||
*/
|
||||
const getProjectStats = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('프로젝트별 통계 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const projectStats = await workAnalysis.getProjectStats(start, end);
|
||||
|
||||
logger.info('프로젝트별 통계 조회 성공', {
|
||||
start,
|
||||
end,
|
||||
projectCount: projectStats.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: projectStats,
|
||||
message: '프로젝트별 통계 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('프로젝트별 통계 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('프로젝트별 통계 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업유형별 통계 조회
|
||||
*/
|
||||
const getWorkTypeStats = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('작업유형별 통계 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workTypeStats = await workAnalysis.getWorkTypeStats(start, end);
|
||||
|
||||
logger.info('작업유형별 통계 조회 성공', {
|
||||
start,
|
||||
end,
|
||||
workTypeCount: workTypeStats.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: workTypeStats,
|
||||
message: '작업유형별 통계 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('작업유형별 통계 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('작업유형별 통계 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 최근 작업 현황 조회
|
||||
*/
|
||||
const getRecentWork = asyncHandler(async (req, res) => {
|
||||
const { start, end, limit = 10 } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
// limit 유효성 검사 (최대 5000까지 허용)
|
||||
const limitNum = parseInt(limit);
|
||||
if (isNaN(limitNum) || limitNum < 1 || limitNum > 5000) {
|
||||
throw new ValidationError('limit은 1~5000 사이의 숫자여야 합니다', {
|
||||
received: limit,
|
||||
min: 1,
|
||||
max: 5000
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('최근 작업 현황 조회 요청', { start, end, limit: limitNum });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const recentWork = await workAnalysis.getRecentWork(start, end, limitNum);
|
||||
|
||||
logger.info('최근 작업 현황 조회 성공', {
|
||||
start,
|
||||
end,
|
||||
limit: limitNum,
|
||||
resultCount: recentWork.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recentWork,
|
||||
message: '최근 작업 현황 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('최근 작업 현황 조회 실패', {
|
||||
start,
|
||||
end,
|
||||
limit: limitNum,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('최근 작업 현황 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 요일별 패턴 분석 조회
|
||||
*/
|
||||
const getWeekdayPattern = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('요일별 패턴 분석 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const weekdayPattern = await workAnalysis.getWeekdayPattern(start, end);
|
||||
|
||||
logger.info('요일별 패턴 분석 성공', { start, end });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weekdayPattern,
|
||||
message: '요일별 패턴 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('요일별 패턴 분석 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('요일별 패턴 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 에러 분석 조회
|
||||
*/
|
||||
const getErrorAnalysis = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('에러 분석 요청', { start, end });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const errorAnalysis = await workAnalysis.getErrorAnalysis(start, end);
|
||||
|
||||
logger.info('에러 분석 성공', { start, end });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: errorAnalysis,
|
||||
message: '에러 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('에러 분석 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('에러 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 월별 비교 분석 조회
|
||||
*/
|
||||
const getMonthlyComparison = asyncHandler(async (req, res) => {
|
||||
const { year = new Date().getFullYear() } = req.query;
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2050) {
|
||||
throw new ValidationError('올바른 연도를 입력해주세요', {
|
||||
received: year,
|
||||
min: 2000,
|
||||
max: 2050
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('월별 비교 분석 요청', { year: yearNum });
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const monthlyData = await workAnalysis.getMonthlyComparison(yearNum);
|
||||
|
||||
logger.info('월별 비교 분석 성공', { year: yearNum });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: monthlyData,
|
||||
message: '월별 비교 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('월별 비교 분석 실패', { year: yearNum, error: error.message });
|
||||
throw new DatabaseError('월별 비교 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자별 전문분야 분석 조회
|
||||
*/
|
||||
const getWorkerSpecialization = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('작업자별 전문분야 분석 요청', { start, end });
|
||||
|
||||
try {
|
||||
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;
|
||||
}, {});
|
||||
|
||||
logger.info('작업자별 전문분야 분석 성공', {
|
||||
start,
|
||||
end,
|
||||
workerCount: Object.keys(groupedData).length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: groupedData,
|
||||
message: '작업자별 전문분야 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('작업자별 전문분야 분석 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('작업자별 전문분야 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 대시보드용 종합 데이터 조회
|
||||
*/
|
||||
const getDashboardData = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('대시보드 데이터 조회 요청', { start, end });
|
||||
|
||||
try {
|
||||
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)
|
||||
]);
|
||||
|
||||
logger.info('대시보드 데이터 조회 성공', { start, end });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
},
|
||||
message: '대시보드 데이터 조회 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('대시보드 데이터 조회 실패', { start, end, error: error.message });
|
||||
throw new DatabaseError('대시보드 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
const workAnalysisService = require('../services/workAnalysisService');
|
||||
|
||||
/**
|
||||
* 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
|
||||
*/
|
||||
const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
validateDateRange(start, end);
|
||||
|
||||
logger.info('프로젝트별-작업별 시간 분석 요청', { start, end });
|
||||
|
||||
try {
|
||||
const result = await workAnalysisService.getProjectWorkTypeAnalysis(start, end);
|
||||
|
||||
logger.info('프로젝트별-작업별 시간 분석 성공', {
|
||||
start,
|
||||
end,
|
||||
projectCount: result.summary.total_projects,
|
||||
workTypeCount: result.summary.total_work_types,
|
||||
totalHours: result.summary.grand_total_hours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '프로젝트별-작업별 시간 분석 완료'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('프로젝트별-작업별 시간 분석 실패', {
|
||||
start,
|
||||
end,
|
||||
error: error.message
|
||||
});
|
||||
// Service throws DatabaseError wrapper or Error
|
||||
if (error.name === 'DatabaseError') {
|
||||
throw error;
|
||||
}
|
||||
throw new DatabaseError('프로젝트별-작업별 시간 분석 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getStats,
|
||||
getDailyTrend,
|
||||
getWorkerStats,
|
||||
getProjectStats,
|
||||
getWorkTypeStats,
|
||||
getRecentWork,
|
||||
getWeekdayPattern,
|
||||
getErrorAnalysis,
|
||||
getMonthlyComparison,
|
||||
getWorkerSpecialization,
|
||||
getDashboardData,
|
||||
getProjectWorkTypeAnalysis
|
||||
};
|
||||
@@ -0,0 +1,674 @@
|
||||
/**
|
||||
* 작업 중 문제 신고 컨트롤러
|
||||
*/
|
||||
|
||||
const workIssueModel = require('../models/workIssueModel');
|
||||
const imageUploadService = require('../services/imageUploadService');
|
||||
|
||||
// ==================== 신고 카테고리 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 카테고리 조회
|
||||
*/
|
||||
exports.getAllCategories = (req, res) => {
|
||||
workIssueModel.getAllCategories((err, categories) => {
|
||||
if (err) {
|
||||
console.error('카테고리 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: categories });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 타입별 카테고리 조회
|
||||
*/
|
||||
exports.getCategoriesByType = (req, res) => {
|
||||
const { type } = req.params;
|
||||
|
||||
if (!['nonconformity', 'safety'].includes(type)) {
|
||||
return res.status(400).json({ success: false, error: '유효하지 않은 카테고리 타입입니다.' });
|
||||
}
|
||||
|
||||
workIssueModel.getCategoriesByType(type, (err, categories) => {
|
||||
if (err) {
|
||||
console.error('카테고리 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: categories });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
exports.createCategory = (req, res) => {
|
||||
const { category_type, category_name, description, display_order } = req.body;
|
||||
|
||||
if (!category_type || !category_name) {
|
||||
return res.status(400).json({ success: false, error: '카테고리 타입과 이름은 필수입니다.' });
|
||||
}
|
||||
|
||||
workIssueModel.createCategory(
|
||||
{ category_type, category_name, description, display_order },
|
||||
(err, categoryId) => {
|
||||
if (err) {
|
||||
console.error('카테고리 생성 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '카테고리 생성 실패' });
|
||||
}
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '카테고리가 생성되었습니다.',
|
||||
data: { category_id: categoryId }
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 수정
|
||||
*/
|
||||
exports.updateCategory = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { category_name, description, display_order, is_active } = req.body;
|
||||
|
||||
workIssueModel.updateCategory(
|
||||
id,
|
||||
{ category_name, description, display_order, is_active },
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
console.error('카테고리 수정 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '카테고리 수정 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '카테고리가 수정되었습니다.' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
exports.deleteCategory = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.deleteCategory(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('카테고리 삭제 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '카테고리가 삭제되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 사전 정의 항목 관리 ====================
|
||||
|
||||
/**
|
||||
* 카테고리별 항목 조회
|
||||
*/
|
||||
exports.getItemsByCategory = (req, res) => {
|
||||
const { categoryId } = req.params;
|
||||
|
||||
workIssueModel.getItemsByCategory(categoryId, (err, items) => {
|
||||
if (err) {
|
||||
console.error('항목 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '항목 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: items });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 항목 조회
|
||||
*/
|
||||
exports.getAllItems = (req, res) => {
|
||||
workIssueModel.getAllItems((err, items) => {
|
||||
if (err) {
|
||||
console.error('항목 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '항목 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: items });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 생성
|
||||
*/
|
||||
exports.createItem = (req, res) => {
|
||||
const { category_id, item_name, description, severity, display_order } = req.body;
|
||||
|
||||
if (!category_id || !item_name) {
|
||||
return res.status(400).json({ success: false, error: '카테고리 ID와 항목명은 필수입니다.' });
|
||||
}
|
||||
|
||||
workIssueModel.createItem(
|
||||
{ category_id, item_name, description, severity, display_order },
|
||||
(err, itemId) => {
|
||||
if (err) {
|
||||
console.error('항목 생성 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '항목 생성 실패' });
|
||||
}
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '항목이 생성되었습니다.',
|
||||
data: { item_id: itemId }
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 수정
|
||||
*/
|
||||
exports.updateItem = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { item_name, description, severity, display_order, is_active } = req.body;
|
||||
|
||||
workIssueModel.updateItem(
|
||||
id,
|
||||
{ item_name, description, severity, display_order, is_active },
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
console.error('항목 수정 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '항목 수정 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '항목이 수정되었습니다.' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 삭제
|
||||
*/
|
||||
exports.deleteItem = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.deleteItem(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('항목 삭제 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '항목 삭제 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '항목이 삭제되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 문제 신고 관리 ====================
|
||||
|
||||
/**
|
||||
* 신고 생성
|
||||
*/
|
||||
exports.createReport = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
factory_category_id,
|
||||
workplace_id,
|
||||
custom_location,
|
||||
tbm_session_id,
|
||||
visit_request_id,
|
||||
issue_category_id,
|
||||
issue_item_id,
|
||||
custom_item_name, // 직접 입력한 항목명
|
||||
additional_description,
|
||||
photos = []
|
||||
} = req.body;
|
||||
|
||||
const reporter_id = req.user.user_id;
|
||||
|
||||
if (!issue_category_id) {
|
||||
return res.status(400).json({ success: false, error: '신고 카테고리는 필수입니다.' });
|
||||
}
|
||||
|
||||
// 위치 정보 검증 (지도 선택 또는 기타 위치)
|
||||
if (!factory_category_id && !custom_location) {
|
||||
return res.status(400).json({ success: false, error: '위치 정보는 필수입니다.' });
|
||||
}
|
||||
|
||||
// 항목 검증 (기존 항목 또는 직접 입력)
|
||||
if (!issue_item_id && !custom_item_name) {
|
||||
return res.status(400).json({ success: false, error: '신고 항목은 필수입니다.' });
|
||||
}
|
||||
|
||||
// 직접 입력한 항목이 있으면 DB에 저장
|
||||
let finalItemId = issue_item_id;
|
||||
if (custom_item_name && !issue_item_id) {
|
||||
try {
|
||||
finalItemId = await new Promise((resolve, reject) => {
|
||||
workIssueModel.createItem(
|
||||
{
|
||||
category_id: issue_category_id,
|
||||
item_name: custom_item_name,
|
||||
description: '사용자 직접 입력',
|
||||
severity: 'medium',
|
||||
display_order: 999 // 마지막에 표시
|
||||
},
|
||||
(err, itemId) => {
|
||||
if (err) reject(err);
|
||||
else resolve(itemId);
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (itemErr) {
|
||||
console.error('커스텀 항목 생성 실패:', itemErr);
|
||||
return res.status(500).json({ success: false, error: '항목 저장 실패' });
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 저장 (최대 5장)
|
||||
const photoPaths = {
|
||||
photo_path1: null,
|
||||
photo_path2: null,
|
||||
photo_path3: null,
|
||||
photo_path4: null,
|
||||
photo_path5: null
|
||||
};
|
||||
|
||||
for (let i = 0; i < Math.min(photos.length, 5); i++) {
|
||||
if (photos[i]) {
|
||||
const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue');
|
||||
if (savedPath) {
|
||||
photoPaths[`photo_path${i + 1}`] = savedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
reporter_id,
|
||||
factory_category_id: factory_category_id || null,
|
||||
workplace_id: workplace_id || null,
|
||||
custom_location: custom_location || null,
|
||||
tbm_session_id: tbm_session_id || null,
|
||||
visit_request_id: visit_request_id || null,
|
||||
issue_category_id,
|
||||
issue_item_id: finalItemId || null,
|
||||
additional_description: additional_description || null,
|
||||
...photoPaths
|
||||
};
|
||||
|
||||
workIssueModel.createReport(reportData, (err, reportId) => {
|
||||
if (err) {
|
||||
console.error('신고 생성 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '신고 생성 실패' });
|
||||
}
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '문제 신고가 등록되었습니다.',
|
||||
data: { report_id: reportId }
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('신고 생성 에러:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 목록 조회
|
||||
*/
|
||||
exports.getAllReports = (req, res) => {
|
||||
const filters = {
|
||||
status: req.query.status,
|
||||
category_type: req.query.category_type,
|
||||
issue_category_id: req.query.issue_category_id,
|
||||
factory_category_id: req.query.factory_category_id,
|
||||
workplace_id: req.query.workplace_id,
|
||||
assigned_user_id: req.query.assigned_user_id,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
search: req.query.search,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset
|
||||
};
|
||||
|
||||
// 일반 사용자는 자신의 신고만 조회 (관리자 제외)
|
||||
const userLevel = req.user.access_level;
|
||||
if (!['admin', 'system', 'support_team'].includes(userLevel)) {
|
||||
filters.reporter_id = req.user.user_id;
|
||||
}
|
||||
|
||||
workIssueModel.getAllReports(filters, (err, reports) => {
|
||||
if (err) {
|
||||
console.error('신고 목록 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: reports });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 상세 조회
|
||||
*/
|
||||
exports.getReportById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.getReportById(id, (err, report) => {
|
||||
if (err) {
|
||||
console.error('신고 상세 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 확인: 본인, 담당자, 또는 관리자
|
||||
const userLevel = req.user.access_level;
|
||||
const isOwner = report.reporter_id === req.user.user_id;
|
||||
const isAssignee = report.assigned_user_id === req.user.user_id;
|
||||
const isManager = ['admin', 'system', 'support_team'].includes(userLevel);
|
||||
|
||||
if (!isOwner && !isAssignee && !isManager) {
|
||||
return res.status(403).json({ success: false, error: '권한이 없습니다.' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: report });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 수정
|
||||
*/
|
||||
exports.updateReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 기존 신고 확인
|
||||
workIssueModel.getReportById(id, async (err, report) => {
|
||||
if (err) {
|
||||
console.error('신고 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '신고 조회 실패' });
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 확인
|
||||
const userLevel = req.user.access_level;
|
||||
const isOwner = report.reporter_id === req.user.user_id;
|
||||
const isManager = ['admin', 'system'].includes(userLevel);
|
||||
|
||||
if (!isOwner && !isManager) {
|
||||
return res.status(403).json({ success: false, error: '수정 권한이 없습니다.' });
|
||||
}
|
||||
|
||||
// 상태 확인: reported 상태에서만 수정 가능 (관리자 제외)
|
||||
if (!isManager && report.status !== 'reported') {
|
||||
return res.status(400).json({ success: false, error: '이미 접수된 신고는 수정할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const {
|
||||
factory_category_id,
|
||||
workplace_id,
|
||||
custom_location,
|
||||
issue_category_id,
|
||||
issue_item_id,
|
||||
additional_description,
|
||||
photos = []
|
||||
} = req.body;
|
||||
|
||||
// 사진 업데이트 처리
|
||||
const photoPaths = {};
|
||||
for (let i = 0; i < Math.min(photos.length, 5); i++) {
|
||||
if (photos[i]) {
|
||||
// 기존 사진 삭제
|
||||
const oldPath = report[`photo_path${i + 1}`];
|
||||
if (oldPath) {
|
||||
await imageUploadService.deleteFile(oldPath);
|
||||
}
|
||||
// 새 사진 저장
|
||||
const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue');
|
||||
if (savedPath) {
|
||||
photoPaths[`photo_path${i + 1}`] = savedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
factory_category_id,
|
||||
workplace_id,
|
||||
custom_location,
|
||||
issue_category_id,
|
||||
issue_item_id,
|
||||
additional_description,
|
||||
...photoPaths
|
||||
};
|
||||
|
||||
workIssueModel.updateReport(id, updateData, req.user.user_id, (updateErr, result) => {
|
||||
if (updateErr) {
|
||||
console.error('신고 수정 실패:', updateErr);
|
||||
return res.status(500).json({ success: false, error: '신고 수정 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '신고가 수정되었습니다.' });
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('신고 수정 에러:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 삭제
|
||||
*/
|
||||
exports.deleteReport = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.getReportById(id, async (err, report) => {
|
||||
if (err) {
|
||||
console.error('신고 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '신고 조회 실패' });
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 확인
|
||||
const userLevel = req.user.access_level;
|
||||
const isOwner = report.reporter_id === req.user.user_id;
|
||||
const isManager = ['admin', 'system'].includes(userLevel);
|
||||
|
||||
if (!isOwner && !isManager) {
|
||||
return res.status(403).json({ success: false, error: '삭제 권한이 없습니다.' });
|
||||
}
|
||||
|
||||
workIssueModel.deleteReport(id, async (deleteErr, { result, photos }) => {
|
||||
if (deleteErr) {
|
||||
console.error('신고 삭제 실패:', deleteErr);
|
||||
return res.status(500).json({ success: false, error: '신고 삭제 실패' });
|
||||
}
|
||||
|
||||
// 사진 파일 삭제
|
||||
if (photos) {
|
||||
const allPhotos = [
|
||||
photos.photo_path1, photos.photo_path2, photos.photo_path3,
|
||||
photos.photo_path4, photos.photo_path5,
|
||||
photos.resolution_photo_path1, photos.resolution_photo_path2
|
||||
].filter(Boolean);
|
||||
await imageUploadService.deleteMultipleFiles(allPhotos);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '신고가 삭제되었습니다.' });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 상태 관리 ====================
|
||||
|
||||
/**
|
||||
* 신고 접수
|
||||
*/
|
||||
exports.receiveReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.receiveReport(id, req.user.user_id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('신고 접수 실패:', err);
|
||||
return res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '신고가 접수되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 담당자 배정
|
||||
*/
|
||||
exports.assignReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { assigned_department, assigned_user_id } = req.body;
|
||||
|
||||
if (!assigned_user_id) {
|
||||
return res.status(400).json({ success: false, error: '담당자는 필수입니다.' });
|
||||
}
|
||||
|
||||
workIssueModel.assignReport(id, {
|
||||
assigned_department,
|
||||
assigned_user_id,
|
||||
assigned_by: req.user.user_id
|
||||
}, (err, result) => {
|
||||
if (err) {
|
||||
console.error('담당자 배정 실패:', err);
|
||||
return res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '담당자가 배정되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 시작
|
||||
*/
|
||||
exports.startProcessing = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.startProcessing(id, req.user.user_id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('처리 시작 실패:', err);
|
||||
return res.status(400).json({ success: false, error: err.message || '처리 시작 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '처리가 시작되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 완료
|
||||
*/
|
||||
exports.completeReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { resolution_notes, resolution_photos = [] } = req.body;
|
||||
|
||||
// 완료 사진 저장
|
||||
let resolution_photo_path1 = null;
|
||||
let resolution_photo_path2 = null;
|
||||
|
||||
if (resolution_photos[0]) {
|
||||
resolution_photo_path1 = await imageUploadService.saveBase64Image(resolution_photos[0], 'resolution');
|
||||
}
|
||||
if (resolution_photos[1]) {
|
||||
resolution_photo_path2 = await imageUploadService.saveBase64Image(resolution_photos[1], 'resolution');
|
||||
}
|
||||
|
||||
workIssueModel.completeReport(id, {
|
||||
resolution_notes,
|
||||
resolution_photo_path1,
|
||||
resolution_photo_path2,
|
||||
resolved_by: req.user.user_id
|
||||
}, (err, result) => {
|
||||
if (err) {
|
||||
console.error('처리 완료 실패:', err);
|
||||
return res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '처리가 완료되었습니다.' });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('처리 완료 에러:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 종료
|
||||
*/
|
||||
exports.closeReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.closeReport(id, req.user.user_id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('신고 종료 실패:', err);
|
||||
return res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
|
||||
}
|
||||
res.json({ success: true, message: '신고가 종료되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 조회
|
||||
*/
|
||||
exports.getStatusLogs = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
workIssueModel.getStatusLogs(id, (err, logs) => {
|
||||
if (err) {
|
||||
console.error('상태 이력 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: logs });
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 통계 ====================
|
||||
|
||||
/**
|
||||
* 통계 요약
|
||||
*/
|
||||
exports.getStatsSummary = (req, res) => {
|
||||
const filters = {
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
factory_category_id: req.query.factory_category_id
|
||||
};
|
||||
|
||||
workIssueModel.getStatsSummary(filters, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '통계 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: stats });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리별 통계
|
||||
*/
|
||||
exports.getStatsByCategory = (req, res) => {
|
||||
const filters = {
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date
|
||||
};
|
||||
|
||||
workIssueModel.getStatsByCategory(filters, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('카테고리별 통계 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '통계 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: stats });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장별 통계
|
||||
*/
|
||||
exports.getStatsByWorkplace = (req, res) => {
|
||||
const filters = {
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
factory_category_id: req.query.factory_category_id
|
||||
};
|
||||
|
||||
workIssueModel.getStatsByWorkplace(filters, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('작업장별 통계 조회 실패:', err);
|
||||
return res.status(500).json({ success: false, error: '통계 조회 실패' });
|
||||
}
|
||||
res.json({ success: true, data: stats });
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 데일리 워크 레포트 분석 컨트롤러
|
||||
*
|
||||
* 작업 보고서 종합 분석 API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const { ValidationError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
|
||||
*/
|
||||
const getAnalysisFilters = asyncHandler(async (req, res) => {
|
||||
logger.info('분석 필터 데이터 조회 요청');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 프로젝트 목록
|
||||
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
|
||||
`);
|
||||
|
||||
logger.info('분석 필터 데이터 조회 성공', {
|
||||
projects: projects.length,
|
||||
workers: workers.length,
|
||||
workTypes: workTypes.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
projects,
|
||||
workers,
|
||||
workTypes,
|
||||
dateRange: dateRange[0]
|
||||
},
|
||||
message: '분석 필터 데이터 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('분석 필터 데이터 조회 실패', { error: error.message });
|
||||
throw new DatabaseError('필터 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 기간별 작업 분석 데이터 조회
|
||||
*/
|
||||
const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
|
||||
const { start_date, end_date, project_id, worker_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
throw new ValidationError('start_date와 end_date가 필요합니다', {
|
||||
required: ['start_date', 'end_date'],
|
||||
received: { start_date, end_date },
|
||||
example: 'start_date=2025-08-01&end_date=2025-08-31'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('기간별 분석 데이터 조회 요청', {
|
||||
start_date,
|
||||
end_date,
|
||||
project_id,
|
||||
worker_id
|
||||
});
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 기본 조건
|
||||
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);
|
||||
|
||||
// 3. 일별 에러 통계
|
||||
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);
|
||||
|
||||
// 4. 에러 유형별 분석
|
||||
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);
|
||||
|
||||
// 5. 작업 유형별 분석
|
||||
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);
|
||||
|
||||
// 6. 작업자별 성과 분석
|
||||
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);
|
||||
|
||||
// 7. 프로젝트별 분석
|
||||
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);
|
||||
|
||||
logger.info('기간별 분석 데이터 조회 성공', {
|
||||
start_date,
|
||||
end_date,
|
||||
total_entries: overallStats[0].total_entries,
|
||||
total_hours: overallStats[0].total_hours
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: overallStats[0],
|
||||
dailyStats,
|
||||
dailyErrorStats,
|
||||
errorAnalysis,
|
||||
workTypeAnalysis,
|
||||
workerAnalysis,
|
||||
projectAnalysis,
|
||||
period: { start_date, end_date },
|
||||
filters: { project_id, worker_id }
|
||||
},
|
||||
message: '기간별 분석 데이터 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('기간별 분석 데이터 조회 실패', {
|
||||
start_date,
|
||||
end_date,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('기간별 분석 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 프로젝트별 상세 분석
|
||||
*/
|
||||
const getProjectAnalysis = asyncHandler(async (req, res) => {
|
||||
const { start_date, end_date, project_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
throw new ValidationError('start_date와 end_date가 필요합니다', {
|
||||
required: ['start_date', 'end_date'],
|
||||
received: { start_date, end_date }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('프로젝트별 분석 조회 요청', {
|
||||
start_date,
|
||||
end_date,
|
||||
project_id
|
||||
});
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
logger.info('프로젝트별 분석 조회 성공', {
|
||||
start_date,
|
||||
end_date,
|
||||
projectCount: projectStats.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
projectStats,
|
||||
period: { start_date, end_date }
|
||||
},
|
||||
message: '프로젝트별 분석 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('프로젝트별 분석 조회 실패', {
|
||||
start_date,
|
||||
end_date,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('프로젝트별 분석 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자별 상세 분석
|
||||
*/
|
||||
const getWorkerAnalysis = asyncHandler(async (req, res) => {
|
||||
const { start_date, end_date, worker_id } = req.query;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
throw new ValidationError('start_date와 end_date가 필요합니다', {
|
||||
required: ['start_date', 'end_date'],
|
||||
received: { start_date, end_date }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('작업자별 분석 조회 요청', {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id
|
||||
});
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
logger.info('작업자별 분석 조회 성공', {
|
||||
start_date,
|
||||
end_date,
|
||||
workerCount: workerStats.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workerStats,
|
||||
period: { start_date, end_date }
|
||||
},
|
||||
message: '작업자별 분석 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('작업자별 분석 조회 실패', {
|
||||
start_date,
|
||||
end_date,
|
||||
error: error.message
|
||||
});
|
||||
throw new DatabaseError('작업자별 분석 데이터 조회 중 오류가 발생했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAnalysisFilters,
|
||||
getAnalyticsByPeriod,
|
||||
getProjectAnalysis,
|
||||
getWorkerAnalysis
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 작업 보고서 관리 컨트롤러
|
||||
*
|
||||
* 작업 보고서 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const workReportService = require('../services/workReportService');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
|
||||
/**
|
||||
* 작업 보고서 생성 (단일 또는 다중)
|
||||
*/
|
||||
exports.createWorkReport = asyncHandler(async (req, res) => {
|
||||
const result = await workReportService.createWorkReportService(req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '작업 보고서가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 날짜별 작업 보고서 조회
|
||||
*/
|
||||
exports.getWorkReportsByDate = asyncHandler(async (req, res) => {
|
||||
const { date } = req.params;
|
||||
const rows = await workReportService.getWorkReportsByDateService(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 기간별 작업 보고서 조회
|
||||
*/
|
||||
exports.getWorkReportsInRange = asyncHandler(async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
const rows = await workReportService.getWorkReportsInRangeService(start, end);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 작업 보고서 조회
|
||||
*/
|
||||
exports.getWorkReportById = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const row = await workReportService.getWorkReportByIdService(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: row,
|
||||
message: '작업 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 보고서 수정
|
||||
*/
|
||||
exports.updateWorkReport = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const result = await workReportService.updateWorkReportService(id, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '작업 보고서가 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 보고서 삭제
|
||||
*/
|
||||
exports.removeWorkReport = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const result = await workReportService.removeWorkReportService(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '작업 보고서가 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 월간 요약 조회
|
||||
*/
|
||||
exports.getSummary = asyncHandler(async (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
const rows = await workReportService.getSummaryService(year, month);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '월간 요약 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 부적합 원인 관리 API ==========
|
||||
|
||||
/**
|
||||
* 작업 보고서의 부적합 원인 목록 조회
|
||||
*/
|
||||
exports.getReportDefects = asyncHandler(async (req, res) => {
|
||||
const { reportId } = req.params;
|
||||
const rows = await workReportService.getReportDefectsService(reportId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '부적합 원인 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 부적합 원인 저장 (전체 교체)
|
||||
* 기존 부적합 원인을 모두 삭제하고 새로 저장
|
||||
*/
|
||||
exports.saveReportDefects = asyncHandler(async (req, res) => {
|
||||
const { reportId } = req.params;
|
||||
const { defects } = req.body; // [{ error_type_id, defect_hours, note }]
|
||||
|
||||
const result = await workReportService.saveReportDefectsService(reportId, defects);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '부적합 원인이 저장되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 부적합 원인 추가 (단일)
|
||||
*/
|
||||
exports.addReportDefect = asyncHandler(async (req, res) => {
|
||||
const { reportId } = req.params;
|
||||
const { error_type_id, defect_hours, note } = req.body;
|
||||
|
||||
const result = await workReportService.addReportDefectService(reportId, {
|
||||
error_type_id,
|
||||
defect_hours,
|
||||
note
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '부적합 원인이 추가되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 부적합 원인 삭제
|
||||
*/
|
||||
exports.removeReportDefect = asyncHandler(async (req, res) => {
|
||||
const { defectId } = req.params;
|
||||
const result = await workReportService.removeReportDefectService(defectId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '부적합 원인이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 작업자 관리 컨트롤러
|
||||
*
|
||||
* 작업자 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2025-12-11
|
||||
*/
|
||||
|
||||
const workerModel = require('../models/workerModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
const cache = require('../utils/cache');
|
||||
const { optimizedQueries } = require('../utils/queryOptimizer');
|
||||
const { hangulToRoman, generateUniqueUsername } = require('../utils/hangulToRoman');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 작업자 생성
|
||||
*/
|
||||
exports.createWorker = asyncHandler(async (req, res) => {
|
||||
const workerData = req.body;
|
||||
const createAccount = req.body.create_account;
|
||||
|
||||
logger.info('작업자 생성 요청', { name: workerData.worker_name, create_account: createAccount });
|
||||
|
||||
const lastID = await workerModel.create(workerData);
|
||||
|
||||
// 계정 생성 요청이 있으면 users 테이블에 계정 생성
|
||||
if (createAccount && workerData.worker_name) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const username = await generateUniqueUsername(workerData.worker_name, db);
|
||||
const hashedPassword = await bcrypt.hash('1234', 10);
|
||||
|
||||
// User 역할 조회
|
||||
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
|
||||
|
||||
if (userRole && userRole.length > 0) {
|
||||
await db.query(
|
||||
`INSERT INTO users (username, password, name, worker_id, role_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[username, hashedPassword, workerData.worker_name, lastID, userRole[0].id]
|
||||
);
|
||||
|
||||
logger.info('작업자 계정 자동 생성 성공', { worker_id: lastID, username });
|
||||
}
|
||||
} catch (accountError) {
|
||||
logger.error('계정 생성 실패 (작업자는 생성됨)', { worker_id: lastID, error: accountError.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
await cache.invalidateCache.worker();
|
||||
|
||||
logger.info('작업자 생성 성공', { worker_id: lastID });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { worker_id: lastID },
|
||||
message: '작업자가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 작업자 조회 (캐싱 및 페이지네이션 적용)
|
||||
*/
|
||||
exports.getAllWorkers = asyncHandler(async (req, res) => {
|
||||
const { page = 1, limit = 100, search = '', status = '', department_id = null } = req.query;
|
||||
|
||||
const cacheKey = cache.createKey('workers', 'list', page, limit, search, status, department_id);
|
||||
|
||||
// 캐시에서 조회
|
||||
const cachedData = await cache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
logger.debug('캐시 히트', { cacheKey });
|
||||
return res.json({
|
||||
success: true,
|
||||
data: cachedData.data,
|
||||
pagination: cachedData.pagination,
|
||||
message: '작업자 목록 조회 성공 (캐시)'
|
||||
});
|
||||
}
|
||||
|
||||
// 최적화된 쿼리 사용
|
||||
const result = await optimizedQueries.getWorkersPaged(page, limit, search, status, department_id);
|
||||
|
||||
// 캐시에 저장 (5분)
|
||||
await cache.set(cacheKey, result, cache.TTL.MEDIUM);
|
||||
logger.debug('캐시 저장', { cacheKey });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
message: '작업자 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 작업자 조회
|
||||
*/
|
||||
exports.getWorkerById = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 작업자 ID입니다');
|
||||
}
|
||||
|
||||
const row = await workerModel.getById(id);
|
||||
|
||||
if (!row) {
|
||||
throw new NotFoundError('작업자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: row,
|
||||
message: '작업자 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자 수정
|
||||
*/
|
||||
exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 작업자 ID입니다');
|
||||
}
|
||||
|
||||
const workerData = { ...req.body, worker_id: id };
|
||||
const createAccount = req.body.create_account;
|
||||
|
||||
console.log('🔧 작업자 수정 요청:', {
|
||||
worker_id: id,
|
||||
받은데이터: req.body,
|
||||
처리할데이터: workerData,
|
||||
create_account: createAccount
|
||||
});
|
||||
|
||||
// 먼저 현재 작업자 정보 조회 (계정 여부 확인용)
|
||||
const currentWorker = await workerModel.getById(id);
|
||||
|
||||
if (!currentWorker) {
|
||||
throw new NotFoundError('작업자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 작업자 정보 업데이트
|
||||
const changes = await workerModel.update(workerData);
|
||||
|
||||
// 계정 생성/해제 처리
|
||||
const db = await getDb();
|
||||
const hasAccount = currentWorker.user_id !== null && currentWorker.user_id !== undefined;
|
||||
let accountAction = null;
|
||||
let accountUsername = null;
|
||||
|
||||
console.log('🔍 계정 생성 체크:', {
|
||||
createAccount,
|
||||
hasAccount,
|
||||
currentWorker_user_id: currentWorker.user_id,
|
||||
worker_name: workerData.worker_name
|
||||
});
|
||||
|
||||
if (createAccount && !hasAccount && workerData.worker_name) {
|
||||
// 계정 생성
|
||||
console.log('✅ 계정 생성 로직 시작');
|
||||
try {
|
||||
console.log('🔑 사용자명 생성 중...');
|
||||
const username = await generateUniqueUsername(workerData.worker_name, db);
|
||||
console.log('🔑 생성된 사용자명:', username);
|
||||
|
||||
const hashedPassword = await bcrypt.hash('1234', 10);
|
||||
console.log('🔒 비밀번호 해싱 완료');
|
||||
|
||||
// User 역할 조회
|
||||
console.log('👤 User 역할 조회 중...');
|
||||
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
|
||||
console.log('👤 User 역할 조회 결과:', userRole);
|
||||
|
||||
if (userRole && userRole.length > 0) {
|
||||
console.log('💾 계정 DB 삽입 시작...');
|
||||
await db.query(
|
||||
`INSERT INTO users (username, password, name, worker_id, role_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[username, hashedPassword, workerData.worker_name, id, userRole[0].id]
|
||||
);
|
||||
console.log('✅ 계정 DB 삽입 완료');
|
||||
|
||||
accountAction = 'created';
|
||||
accountUsername = username;
|
||||
logger.info('작업자 계정 생성 성공', { worker_id: id, username });
|
||||
} else {
|
||||
console.log('❌ User 역할을 찾을 수 없음');
|
||||
}
|
||||
} catch (accountError) {
|
||||
console.error('❌ 계정 생성 오류:', accountError);
|
||||
logger.error('계정 생성 실패', { worker_id: id, error: accountError.message });
|
||||
accountAction = 'failed';
|
||||
}
|
||||
} else {
|
||||
console.log('⏭️ 계정 생성 조건 불만족:', { createAccount, hasAccount, hasWorkerName: !!workerData.worker_name });
|
||||
}
|
||||
|
||||
if (!createAccount && hasAccount) {
|
||||
// 계정 연동 해제 (users.worker_id = NULL)
|
||||
try {
|
||||
await db.query('UPDATE users SET worker_id = NULL WHERE worker_id = ?', [id]);
|
||||
accountAction = 'unlinked';
|
||||
logger.info('작업자 계정 연동 해제 성공', { worker_id: id });
|
||||
} catch (unlinkError) {
|
||||
logger.error('계정 연동 해제 실패', { worker_id: id, error: unlinkError.message });
|
||||
accountAction = 'unlink_failed';
|
||||
}
|
||||
} else if (createAccount && hasAccount) {
|
||||
accountAction = 'already_exists';
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
logger.info('작업자 수정 후 캐시 무효화', { worker_id: id });
|
||||
await cache.invalidateCache.worker();
|
||||
|
||||
logger.info('작업자 수정 성공', { worker_id: id });
|
||||
|
||||
// 응답 메시지 구성
|
||||
let message = '작업자 정보가 성공적으로 수정되었습니다';
|
||||
if (accountAction === 'created') {
|
||||
message += ` (계정 생성 완료: ${accountUsername}, 초기 비밀번호: 1234)`;
|
||||
} else if (accountAction === 'unlinked') {
|
||||
message += ' (계정 연동 해제 완료)';
|
||||
} else if (accountAction === 'already_exists') {
|
||||
message += ' (이미 계정이 존재합니다)';
|
||||
} else if (accountAction === 'failed') {
|
||||
message += ' (계정 생성 실패)';
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
changes,
|
||||
account_action: accountAction,
|
||||
account_username: accountUsername
|
||||
},
|
||||
message
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업자 삭제
|
||||
*/
|
||||
exports.removeWorker = asyncHandler(async (req, res) => {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 작업자 ID입니다');
|
||||
}
|
||||
|
||||
const changes = await workerModel.remove(id);
|
||||
|
||||
if (changes === 0) {
|
||||
throw new NotFoundError('작업자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
logger.info('작업자 삭제 후 캐시 무효화 시작', { worker_id: id });
|
||||
await cache.invalidateCache.worker();
|
||||
await cache.delPattern('workers:*');
|
||||
await cache.flush();
|
||||
logger.info('작업자 삭제 후 캐시 무효화 완료', { worker_id: id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업자가 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,575 @@
|
||||
/**
|
||||
* 작업장 관리 컨트롤러
|
||||
*
|
||||
* 작업장 카테고리(공장) 및 작업장 CRUD API 엔드포인트 핸들러
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-26
|
||||
*/
|
||||
|
||||
const workplaceModel = require('../models/workplaceModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// ==================== 카테고리(공장) 관련 ====================
|
||||
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
exports.createCategory = asyncHandler(async (req, res) => {
|
||||
const categoryData = req.body;
|
||||
|
||||
if (!categoryData.category_name) {
|
||||
throw new ValidationError('카테고리명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('카테고리 생성 요청', { name: categoryData.category_name });
|
||||
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
workplaceModel.createCategory(categoryData, (err, lastID) => {
|
||||
if (err) reject(new DatabaseError('카테고리 생성 중 오류가 발생했습니다'));
|
||||
else resolve(lastID);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('카테고리 생성 성공', { category_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { category_id: id },
|
||||
message: '카테고리가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 카테고리 조회
|
||||
*/
|
||||
exports.getAllCategories = asyncHandler(async (req, res) => {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getAllCategories((err, data) => {
|
||||
if (err) reject(new DatabaseError('카테고리 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '카테고리 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 활성 카테고리만 조회
|
||||
*/
|
||||
exports.getActiveCategories = asyncHandler(async (req, res) => {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getActiveCategories((err, data) => {
|
||||
if (err) reject(new DatabaseError('활성 카테고리 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '활성 카테고리 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 카테고리 조회
|
||||
*/
|
||||
exports.getCategoryById = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.params.id;
|
||||
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: category,
|
||||
message: '카테고리 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 수정
|
||||
*/
|
||||
exports.updateCategory = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.params.id;
|
||||
const categoryData = req.body;
|
||||
|
||||
if (!categoryData.category_name) {
|
||||
throw new ValidationError('카테고리명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('카테고리 수정 요청', { category_id: categoryId });
|
||||
|
||||
// 기존 카테고리 정보 가져오기
|
||||
const existingCategory = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!existingCategory) {
|
||||
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// layout_image가 요청에 없거나 null이면 기존 값 보존
|
||||
const updateData = {
|
||||
...categoryData,
|
||||
layout_image: (categoryData.layout_image !== undefined && categoryData.layout_image !== null)
|
||||
? categoryData.layout_image
|
||||
: existingCategory.layout_image
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.updateCategory(categoryId, updateData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('카테고리 수정 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('카테고리 수정 성공', { category_id: categoryId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '카테고리가 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
exports.deleteCategory = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.params.id;
|
||||
|
||||
logger.info('카테고리 삭제 요청', { category_id: categoryId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.deleteCategory(categoryId, (err, result) => {
|
||||
if (err) reject(new DatabaseError('카테고리 삭제 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('카테고리 삭제 성공', { category_id: categoryId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '카테고리가 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 작업장 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업장 생성
|
||||
*/
|
||||
exports.createWorkplace = asyncHandler(async (req, res) => {
|
||||
const workplaceData = req.body;
|
||||
|
||||
if (!workplaceData.workplace_name) {
|
||||
throw new ValidationError('작업장명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업장 생성 요청', { name: workplaceData.workplace_name });
|
||||
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
workplaceModel.createWorkplace(workplaceData, (err, lastID) => {
|
||||
if (err) reject(new DatabaseError('작업장 생성 중 오류가 발생했습니다'));
|
||||
else resolve(lastID);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업장 생성 성공', { workplace_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { workplace_id: id },
|
||||
message: '작업장이 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 작업장 조회
|
||||
*/
|
||||
exports.getAllWorkplaces = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.query.category_id;
|
||||
|
||||
// 카테고리별 필터링
|
||||
if (categoryId) {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getWorkplacesByCategory(categoryId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업장 목록 조회 성공'
|
||||
});
|
||||
}
|
||||
|
||||
// 전체 조회
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getAllWorkplaces((err, data) => {
|
||||
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업장 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 활성 작업장만 조회
|
||||
*/
|
||||
exports.getActiveWorkplaces = asyncHandler(async (req, res) => {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getActiveWorkplaces((err, data) => {
|
||||
if (err) reject(new DatabaseError('활성 작업장 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '활성 작업장 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 작업장 조회
|
||||
*/
|
||||
exports.getWorkplaceById = asyncHandler(async (req, res) => {
|
||||
const workplaceId = req.params.id;
|
||||
|
||||
const workplace = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!workplace) {
|
||||
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: workplace,
|
||||
message: '작업장 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업장 수정
|
||||
*/
|
||||
exports.updateWorkplace = asyncHandler(async (req, res) => {
|
||||
const workplaceId = req.params.id;
|
||||
const workplaceData = req.body;
|
||||
|
||||
if (!workplaceData.workplace_name) {
|
||||
throw new ValidationError('작업장명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업장 수정 요청', { workplace_id: workplaceId });
|
||||
|
||||
// 기존 작업장 정보 가져오기
|
||||
const existingWorkplace = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!existingWorkplace) {
|
||||
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// layout_image가 요청에 없거나 null이면 기존 값 보존
|
||||
const updateData = {
|
||||
...workplaceData,
|
||||
layout_image: (workplaceData.layout_image !== undefined && workplaceData.layout_image !== null)
|
||||
? workplaceData.layout_image
|
||||
: existingWorkplace.layout_image
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.updateWorkplace(workplaceId, updateData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('작업장 수정 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업장 수정 성공', { workplace_id: workplaceId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업장이 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업장 삭제
|
||||
*/
|
||||
exports.deleteWorkplace = asyncHandler(async (req, res) => {
|
||||
const workplaceId = req.params.id;
|
||||
|
||||
logger.info('작업장 삭제 요청', { workplace_id: workplaceId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.deleteWorkplace(workplaceId, (err, result) => {
|
||||
if (err) reject(new DatabaseError('작업장 삭제 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업장 삭제 성공', { workplace_id: workplaceId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업장이 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 작업장 지도 영역 관련 ====================
|
||||
|
||||
/**
|
||||
* 카테고리 레이아웃 이미지 업로드
|
||||
*/
|
||||
exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.params.id;
|
||||
|
||||
if (!req.file) {
|
||||
throw new ValidationError('이미지 파일이 필요합니다');
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
|
||||
logger.info('카테고리 레이아웃 이미지 업로드 요청', { category_id: categoryId, path: imagePath });
|
||||
|
||||
// 현재 카테고리 정보 가져오기
|
||||
const category = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 카테고리 정보 업데이트 (이미지 경로만 변경)
|
||||
const updatedData = {
|
||||
category_name: category.category_name,
|
||||
description: category.description,
|
||||
display_order: category.display_order,
|
||||
is_active: category.is_active,
|
||||
layout_image: imagePath
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.updateCategory(categoryId, updatedData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('레이아웃 이미지 업로드 성공', { category_id: categoryId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { image_path: imagePath },
|
||||
message: '레이아웃 이미지가 성공적으로 업로드되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업장 레이아웃 이미지 업로드
|
||||
*/
|
||||
exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
|
||||
const workplaceId = req.params.id;
|
||||
|
||||
if (!req.file) {
|
||||
throw new ValidationError('이미지 파일이 필요합니다');
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
|
||||
logger.info('작업장 레이아웃 이미지 업로드 요청', { workplace_id: workplaceId, path: imagePath });
|
||||
|
||||
// 현재 작업장 정보 가져오기
|
||||
const workplace = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!workplace) {
|
||||
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 작업장 정보 업데이트 (이미지 경로만 변경)
|
||||
const updatedData = {
|
||||
workplace_name: workplace.workplace_name,
|
||||
category_id: workplace.category_id,
|
||||
description: workplace.description,
|
||||
workplace_purpose: workplace.workplace_purpose,
|
||||
display_priority: workplace.display_priority,
|
||||
is_active: workplace.is_active,
|
||||
layout_image: imagePath
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.updateWorkplace(workplaceId, updatedData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업장 레이아웃 이미지 업로드 성공', { workplace_id: workplaceId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { image_path: imagePath },
|
||||
message: '작업장 레이아웃 이미지가 성공적으로 업로드되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 지도 영역 생성
|
||||
*/
|
||||
exports.createMapRegion = asyncHandler(async (req, res) => {
|
||||
const regionData = req.body;
|
||||
|
||||
if (!regionData.workplace_id || !regionData.category_id) {
|
||||
throw new ValidationError('작업장 ID와 카테고리 ID는 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('지도 영역 생성 요청', { workplace_id: regionData.workplace_id });
|
||||
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
workplaceModel.createMapRegion(regionData, (err, lastID) => {
|
||||
if (err) reject(new DatabaseError('지도 영역 생성 중 오류가 발생했습니다'));
|
||||
else resolve(lastID);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('지도 영역 생성 성공', { region_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { region_id: id },
|
||||
message: '지도 영역이 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리별 지도 영역 조회 (작업장 정보 포함)
|
||||
*/
|
||||
exports.getMapRegionsByCategory = asyncHandler(async (req, res) => {
|
||||
const categoryId = req.params.categoryId;
|
||||
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getMapRegionsByCategory(categoryId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '지도 영역 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업장별 지도 영역 조회
|
||||
*/
|
||||
exports.getMapRegionByWorkplace = asyncHandler(async (req, res) => {
|
||||
const workplaceId = req.params.workplaceId;
|
||||
|
||||
const region = await new Promise((resolve, reject) => {
|
||||
workplaceModel.getMapRegionByWorkplace(workplaceId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: region,
|
||||
message: '지도 영역 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 지도 영역 수정
|
||||
*/
|
||||
exports.updateMapRegion = asyncHandler(async (req, res) => {
|
||||
const regionId = req.params.id;
|
||||
const regionData = req.body;
|
||||
|
||||
logger.info('지도 영역 수정 요청', { region_id: regionId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.updateMapRegion(regionId, regionData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('지도 영역 수정 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('지도 영역 수정 성공', { region_id: regionId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '지도 영역이 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 지도 영역 삭제
|
||||
*/
|
||||
exports.deleteMapRegion = asyncHandler(async (req, res) => {
|
||||
const regionId = req.params.id;
|
||||
|
||||
logger.info('지도 영역 삭제 요청', { region_id: regionId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
workplaceModel.deleteMapRegion(regionId, (err, result) => {
|
||||
if (err) reject(new DatabaseError('지도 영역 삭제 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('지도 영역 삭제 성공', { region_id: regionId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '지도 영역이 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user