11개 모델 파일의 171개 콜백 메서드를 직접 return/throw 패턴으로 변환. 8개 컨트롤러에서 new Promise 래퍼와 중첩 콜백 제거, console.error→logger.error 교체. 미사용 pageAccessModel.js 삭제. 전체 -3,600줄 감소. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
524 lines
17 KiB
JavaScript
524 lines
17 KiB
JavaScript
/**
|
|
* 작업 중 문제 신고 컨트롤러
|
|
*/
|
|
|
|
const workIssueModel = require('../models/workIssueModel');
|
|
const imageUploadService = require('../services/imageUploadService');
|
|
const logger = require('../utils/logger');
|
|
|
|
// ==================== 신고 카테고리 관리 ====================
|
|
|
|
exports.getAllCategories = async (req, res) => {
|
|
try {
|
|
const categories = await workIssueModel.getAllCategories();
|
|
res.json({ success: true, data: categories });
|
|
} catch (err) {
|
|
logger.error('카테고리 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getCategoriesByType = async (req, res) => {
|
|
try {
|
|
const { type } = req.params;
|
|
|
|
if (!['nonconformity', 'safety', 'facility'].includes(type)) {
|
|
return res.status(400).json({ success: false, error: '유효하지 않은 카테고리 타입입니다.' });
|
|
}
|
|
|
|
const categories = await workIssueModel.getCategoriesByType(type);
|
|
res.json({ success: true, data: categories });
|
|
} catch (err) {
|
|
logger.error('카테고리 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.createCategory = async (req, res) => {
|
|
try {
|
|
const { category_type, category_name, description, display_order } = req.body;
|
|
|
|
if (!category_type || !category_name) {
|
|
return res.status(400).json({ success: false, error: '카테고리 타입과 이름은 필수입니다.' });
|
|
}
|
|
|
|
const categoryId = await workIssueModel.createCategory({ category_type, category_name, description, display_order });
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '카테고리가 생성되었습니다.',
|
|
data: { category_id: categoryId }
|
|
});
|
|
} catch (err) {
|
|
logger.error('카테고리 생성 실패:', err);
|
|
res.status(500).json({ success: false, error: '카테고리 생성 실패' });
|
|
}
|
|
};
|
|
|
|
exports.updateCategory = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { category_name, description, display_order, is_active } = req.body;
|
|
|
|
await workIssueModel.updateCategory(id, { category_name, description, display_order, is_active });
|
|
res.json({ success: true, message: '카테고리가 수정되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('카테고리 수정 실패:', err);
|
|
res.status(500).json({ success: false, error: '카테고리 수정 실패' });
|
|
}
|
|
};
|
|
|
|
exports.deleteCategory = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await workIssueModel.deleteCategory(id);
|
|
res.json({ success: true, message: '카테고리가 삭제되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('카테고리 삭제 실패:', err);
|
|
res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
|
|
}
|
|
};
|
|
|
|
// ==================== 사전 정의 항목 관리 ====================
|
|
|
|
exports.getItemsByCategory = async (req, res) => {
|
|
try {
|
|
const { categoryId } = req.params;
|
|
const items = await workIssueModel.getItemsByCategory(categoryId);
|
|
res.json({ success: true, data: items });
|
|
} catch (err) {
|
|
logger.error('항목 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '항목 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getAllItems = async (req, res) => {
|
|
try {
|
|
const items = await workIssueModel.getAllItems();
|
|
res.json({ success: true, data: items });
|
|
} catch (err) {
|
|
logger.error('항목 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '항목 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.createItem = async (req, res) => {
|
|
try {
|
|
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와 항목명은 필수입니다.' });
|
|
}
|
|
|
|
const itemId = await workIssueModel.createItem({ category_id, item_name, description, severity, display_order });
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '항목이 생성되었습니다.',
|
|
data: { item_id: itemId }
|
|
});
|
|
} catch (err) {
|
|
logger.error('항목 생성 실패:', err);
|
|
res.status(500).json({ success: false, error: '항목 생성 실패' });
|
|
}
|
|
};
|
|
|
|
exports.updateItem = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { item_name, description, severity, display_order, is_active } = req.body;
|
|
|
|
await workIssueModel.updateItem(id, { item_name, description, severity, display_order, is_active });
|
|
res.json({ success: true, message: '항목이 수정되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('항목 수정 실패:', err);
|
|
res.status(500).json({ success: false, error: '항목 수정 실패' });
|
|
}
|
|
};
|
|
|
|
exports.deleteItem = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await workIssueModel.deleteItem(id);
|
|
res.json({ success: true, message: '항목이 삭제되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('항목 삭제 실패:', err);
|
|
res.status(500).json({ success: false, error: '항목 삭제 실패' });
|
|
}
|
|
};
|
|
|
|
// ==================== 문제 신고 관리 ====================
|
|
|
|
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: '신고 항목은 필수입니다.' });
|
|
}
|
|
|
|
let finalItemId = issue_item_id;
|
|
if (custom_item_name && !issue_item_id) {
|
|
finalItemId = await workIssueModel.createItem({
|
|
category_id: issue_category_id,
|
|
item_name: custom_item_name,
|
|
description: '사용자 직접 입력',
|
|
severity: 'medium',
|
|
display_order: 999
|
|
});
|
|
}
|
|
|
|
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
|
|
};
|
|
|
|
const reportId = await workIssueModel.createReport(reportData);
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '문제 신고가 등록되었습니다.',
|
|
data: { report_id: reportId }
|
|
});
|
|
} catch (err) {
|
|
logger.error('신고 생성 실패:', err);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
|
}
|
|
};
|
|
|
|
exports.getAllReports = async (req, res) => {
|
|
try {
|
|
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;
|
|
}
|
|
|
|
const reports = await workIssueModel.getAllReports(filters);
|
|
res.json({ success: true, data: reports });
|
|
} catch (err) {
|
|
logger.error('신고 목록 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getReportById = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const report = await workIssueModel.getReportById(id);
|
|
|
|
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 });
|
|
} catch (err) {
|
|
logger.error('신고 상세 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.updateReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const report = await workIssueModel.getReportById(id);
|
|
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: '수정 권한이 없습니다.' });
|
|
}
|
|
|
|
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
|
|
};
|
|
|
|
await workIssueModel.updateReport(id, updateData, req.user.user_id);
|
|
res.json({ success: true, message: '신고가 수정되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('신고 수정 실패:', err);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
|
}
|
|
};
|
|
|
|
exports.deleteReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const report = await workIssueModel.getReportById(id);
|
|
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: '삭제 권한이 없습니다.' });
|
|
}
|
|
|
|
const { photos } = await workIssueModel.deleteReport(id);
|
|
|
|
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: '신고가 삭제되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('신고 삭제 실패:', err);
|
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
|
|
}
|
|
};
|
|
|
|
// ==================== 상태 관리 ====================
|
|
|
|
exports.receiveReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await workIssueModel.receiveReport(id, req.user.user_id);
|
|
res.json({ success: true, message: '신고가 접수되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('신고 접수 실패:', err);
|
|
res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
|
|
}
|
|
};
|
|
|
|
exports.assignReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { assigned_department, assigned_user_id } = req.body;
|
|
|
|
if (!assigned_user_id) {
|
|
return res.status(400).json({ success: false, error: '담당자는 필수입니다.' });
|
|
}
|
|
|
|
await workIssueModel.assignReport(id, {
|
|
assigned_department,
|
|
assigned_user_id,
|
|
assigned_by: req.user.user_id
|
|
});
|
|
res.json({ success: true, message: '담당자가 배정되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('담당자 배정 실패:', err);
|
|
res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
|
|
}
|
|
};
|
|
|
|
exports.startProcessing = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await workIssueModel.startProcessing(id, req.user.user_id);
|
|
res.json({ success: true, message: '처리가 시작되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('처리 시작 실패:', err);
|
|
res.status(400).json({ success: false, error: err.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');
|
|
}
|
|
|
|
await workIssueModel.completeReport(id, {
|
|
resolution_notes,
|
|
resolution_photo_path1,
|
|
resolution_photo_path2,
|
|
resolved_by: req.user.user_id
|
|
});
|
|
res.json({ success: true, message: '처리가 완료되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('처리 완료 실패:', err);
|
|
res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
|
|
}
|
|
};
|
|
|
|
exports.closeReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await workIssueModel.closeReport(id, req.user.user_id);
|
|
res.json({ success: true, message: '신고가 종료되었습니다.' });
|
|
} catch (err) {
|
|
logger.error('신고 종료 실패:', err);
|
|
res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getStatusLogs = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const logs = await workIssueModel.getStatusLogs(id);
|
|
res.json({ success: true, data: logs });
|
|
} catch (err) {
|
|
logger.error('상태 이력 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
|
|
}
|
|
};
|
|
|
|
// ==================== 통계 ====================
|
|
|
|
exports.getStatsSummary = async (req, res) => {
|
|
try {
|
|
const filters = {
|
|
start_date: req.query.start_date,
|
|
end_date: req.query.end_date,
|
|
factory_category_id: req.query.factory_category_id
|
|
};
|
|
const stats = await workIssueModel.getStatsSummary(filters);
|
|
res.json({ success: true, data: stats });
|
|
} catch (err) {
|
|
logger.error('통계 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '통계 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getStatsByCategory = async (req, res) => {
|
|
try {
|
|
const filters = {
|
|
start_date: req.query.start_date,
|
|
end_date: req.query.end_date
|
|
};
|
|
const stats = await workIssueModel.getStatsByCategory(filters);
|
|
res.json({ success: true, data: stats });
|
|
} catch (err) {
|
|
logger.error('카테고리별 통계 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '통계 조회 실패' });
|
|
}
|
|
};
|
|
|
|
exports.getStatsByWorkplace = async (req, res) => {
|
|
try {
|
|
const filters = {
|
|
start_date: req.query.start_date,
|
|
end_date: req.query.end_date,
|
|
factory_category_id: req.query.factory_category_id
|
|
};
|
|
const stats = await workIssueModel.getStatsByWorkplace(filters);
|
|
res.json({ success: true, data: stats });
|
|
} catch (err) {
|
|
logger.error('작업장별 통계 조회 실패:', err);
|
|
res.status(500).json({ success: false, error: '통계 조회 실패' });
|
|
}
|
|
};
|