Files
TK-FB-Project/api.hyungi.net/controllers/workIssueController.js
Hyungi Ahn 74d3a78aa3 feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성
  - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동
  - common/ → attendance/: 근태/휴가 관련 페이지 이동
  - admin/ 정리: safety-* 파일들을 safety/로 이동

- 사이드바 네비게이션 메뉴 구현
  - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리
  - 접기/펼치기 기능 및 상태 저장
  - 관리자 전용 메뉴 자동 표시/숨김

- 날씨 API 연동 (기상청 단기예보)
  - TBM 및 navbar에 현재 날씨 표시
  - weatherService.js 추가

- 안전 체크리스트 확장
  - 기본/날씨별/작업별 체크 유형 추가
  - checklist-manage.html 페이지 추가

- 이슈 신고 시스템 구현
  - workIssueController, workIssueModel, workIssueRoutes 추가

- DB 마이그레이션 파일 추가 (실행 대기)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:27:22 +09:00

644 lines
19 KiB
JavaScript

/**
* 작업 중 문제 신고 컨트롤러
*/
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,
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: '위치 정보는 필수입니다.' });
}
// 사진 저장 (최대 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: issue_item_id || 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 });
});
};