feat: 권한 탭 분리 + 부서 인원 표시 + 다수 시스템 개선
- tkuser: 권한 관리를 별도 탭으로 분리, 부서 클릭 시 소속 인원 목록 표시 - system1: 모바일 UI 개선, nginx 권한 보정, 신고 카테고리 타입 마이그레이션 - system2: 신고 상세/보고서 개선, 내 보고서 페이지 추가 - system3: 이슈 뷰/수신함/관리함 개선 - gateway: 포털 라우팅 수정 - user-management API: 부서별 권한 벌크 설정 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -272,6 +272,20 @@ exports.createReport = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// category_type 조회
|
||||
let categoryType = null;
|
||||
try {
|
||||
const catInfo = await new Promise((resolve, reject) => {
|
||||
workIssueModel.getCategoryById(issue_category_id, (catErr, data) => {
|
||||
if (catErr) reject(catErr); else resolve(data);
|
||||
});
|
||||
});
|
||||
if (catInfo) categoryType = catInfo.category_type;
|
||||
} catch (catErr) {
|
||||
console.error('카테고리 조회 실패:', catErr);
|
||||
return res.status(500).json({ success: false, error: '카테고리 정보 조회 실패' });
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
reporter_id,
|
||||
factory_category_id: factory_category_id || null,
|
||||
@@ -282,6 +296,7 @@ exports.createReport = async (req, res) => {
|
||||
visit_request_id: visit_request_id || null,
|
||||
issue_category_id,
|
||||
issue_item_id: finalItemId || null,
|
||||
category_type: categoryType,
|
||||
additional_description: additional_description || null,
|
||||
...photoPaths
|
||||
};
|
||||
@@ -326,6 +341,26 @@ exports.createReport = async (req, res) => {
|
||||
|
||||
const descText = additional_description || categoryInfo.category_name;
|
||||
|
||||
// 위치 정보 조회
|
||||
let locationInfo = custom_location || null;
|
||||
if (factory_category_id) {
|
||||
try {
|
||||
const locationParts = await new Promise((resolve, reject) => {
|
||||
workIssueModel.getLocationNames(factory_category_id, workplace_id, (locErr, data) => {
|
||||
if (locErr) reject(locErr); else resolve(data);
|
||||
});
|
||||
});
|
||||
if (locationParts) {
|
||||
locationInfo = locationParts.factory_name || '';
|
||||
if (locationParts.workplace_name) {
|
||||
locationInfo += ` - ${locationParts.workplace_name}`;
|
||||
}
|
||||
}
|
||||
} catch (locErr) {
|
||||
console.error('위치 정보 조회 실패:', locErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 원래 신고자의 SSO 토큰 추출
|
||||
const originalToken = (req.headers['authorization'] || '').replace('Bearer ', '');
|
||||
|
||||
@@ -335,6 +370,7 @@ exports.createReport = async (req, res) => {
|
||||
reporter_name: req.user.name || req.user.username,
|
||||
tk_issue_id: reportId,
|
||||
project_id: project_id || null,
|
||||
location_info: locationInfo,
|
||||
photos: photoBase64List,
|
||||
ssoToken: originalToken
|
||||
});
|
||||
@@ -667,6 +703,30 @@ exports.getStatusLogs = (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 유형 이관 ====================
|
||||
|
||||
/**
|
||||
* 신고 유형 이관
|
||||
*/
|
||||
exports.transferReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { category_type } = req.body;
|
||||
|
||||
// ENUM 유효성 검증
|
||||
const validTypes = ['nonconformity', 'safety', 'facility'];
|
||||
if (!category_type || !validTypes.includes(category_type)) {
|
||||
return res.status(400).json({ success: false, error: '유효하지 않은 유형입니다. (nonconformity, safety, facility)' });
|
||||
}
|
||||
|
||||
workIssueModel.transferCategoryType(id, category_type, 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: '유형이 이관되었습니다.' });
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 통계 ====================
|
||||
|
||||
/**
|
||||
@@ -674,6 +734,7 @@ exports.getStatusLogs = (req, res) => {
|
||||
*/
|
||||
exports.getStatsSummary = (req, res) => {
|
||||
const filters = {
|
||||
category_type: req.query.category_type,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
factory_category_id: req.query.factory_category_id
|
||||
|
||||
@@ -237,6 +237,7 @@ const createReport = async (reportData, callback) => {
|
||||
visit_request_id = null,
|
||||
issue_category_id,
|
||||
issue_item_id = null,
|
||||
category_type,
|
||||
additional_description = null,
|
||||
photo_path1 = null,
|
||||
photo_path2 = null,
|
||||
@@ -251,11 +252,11 @@ const createReport = async (reportData, callback) => {
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO work_issue_reports
|
||||
(reporter_id, report_date, factory_category_id, workplace_id, project_id, custom_location,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id, category_type,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[reporter_id, reportDate, factory_category_id, workplace_id, project_id, custom_location,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id, category_type,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5]
|
||||
);
|
||||
|
||||
@@ -291,7 +292,7 @@ const getAllReports = async (filters = {}, callback) => {
|
||||
u.username as reporter_name, u.name as reporter_full_name,
|
||||
wc.category_name as factory_name,
|
||||
w.workplace_name,
|
||||
irc.category_type, irc.category_name as issue_category_name,
|
||||
wir.category_type, irc.category_type as original_category_type, irc.category_name as issue_category_name,
|
||||
iri.item_name as issue_item_name, iri.severity,
|
||||
assignee.username as assigned_user_name, assignee.name as assigned_full_name
|
||||
FROM work_issue_reports wir
|
||||
@@ -313,7 +314,7 @@ const getAllReports = async (filters = {}, callback) => {
|
||||
}
|
||||
|
||||
if (filters.category_type) {
|
||||
query += ` AND irc.category_type = ?`;
|
||||
query += ` AND wir.category_type = ?`;
|
||||
params.push(filters.category_type);
|
||||
}
|
||||
|
||||
@@ -394,7 +395,7 @@ const getReportById = async (reportId, callback) => {
|
||||
u.username as reporter_name, u.name as reporter_full_name,
|
||||
wc.category_name as factory_name,
|
||||
w.workplace_name,
|
||||
irc.category_type, irc.category_name as issue_category_name,
|
||||
wir.category_type, irc.category_type as original_category_type, irc.category_name as issue_category_name,
|
||||
iri.item_name as issue_item_name, iri.severity,
|
||||
assignee.username as assigned_user_name, assignee.name as assigned_full_name,
|
||||
assigner.username as assigned_by_name,
|
||||
@@ -783,25 +784,30 @@ const getStatsSummary = async (filters = {}, callback) => {
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.category_type) {
|
||||
whereClause += ` AND wir.category_type = ?`;
|
||||
params.push(filters.category_type);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(report_date) BETWEEN ? AND ?`;
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
whereClause += ` AND factory_category_id = ?`;
|
||||
whereClause += ` AND wir.factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'reported' THEN 1 ELSE 0 END) as reported,
|
||||
SUM(CASE WHEN status = 'received' THEN 1 ELSE 0 END) as received,
|
||||
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed
|
||||
FROM work_issue_reports
|
||||
SUM(CASE WHEN wir.status = 'reported' THEN 1 ELSE 0 END) as reported,
|
||||
SUM(CASE WHEN wir.status = 'received' THEN 1 ELSE 0 END) as received,
|
||||
SUM(CASE WHEN wir.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
||||
SUM(CASE WHEN wir.status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN wir.status = 'closed' THEN 1 ELSE 0 END) as closed
|
||||
FROM work_issue_reports wir
|
||||
WHERE ${whereClause}`,
|
||||
params
|
||||
);
|
||||
@@ -885,6 +891,86 @@ const getStatsByWorkplace = async (filters = {}, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 유형 이관 (category_type 변경)
|
||||
*/
|
||||
const transferCategoryType = async (reportId, newCategoryType, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 데이터 조회
|
||||
const [existing] = await db.query(
|
||||
`SELECT report_id, category_type, modification_history FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
const current = existing[0];
|
||||
const oldCategoryType = current.category_type;
|
||||
|
||||
if (oldCategoryType === newCategoryType) {
|
||||
return callback(new Error('현재 유형과 동일합니다.'));
|
||||
}
|
||||
|
||||
// 수정 이력 추가
|
||||
const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : [];
|
||||
const now = new Date().toISOString();
|
||||
existingHistory.push({
|
||||
field: 'category_type',
|
||||
old_value: oldCategoryType,
|
||||
new_value: newCategoryType,
|
||||
modified_at: now,
|
||||
modified_by: userId
|
||||
});
|
||||
|
||||
// category_type 업데이트
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET category_type = ?, modification_history = ?, updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[newCategoryType, JSON.stringify(existingHistory), reportId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 공장/작업장 이름 조회 (System 3 연동용)
|
||||
*/
|
||||
const getLocationNames = async (factoryCategoryId, workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let factoryName = null;
|
||||
let workplaceName = null;
|
||||
|
||||
if (factoryCategoryId) {
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_name FROM workplace_categories WHERE category_id = ?`,
|
||||
[factoryCategoryId]
|
||||
);
|
||||
if (rows.length > 0) factoryName = rows[0].category_name;
|
||||
}
|
||||
|
||||
if (workplaceId) {
|
||||
const [rows] = await db.query(
|
||||
`SELECT workplace_name FROM workplaces WHERE workplace_id = ?`,
|
||||
[workplaceId]
|
||||
);
|
||||
if (rows.length > 0) workplaceName = rows[0].workplace_name;
|
||||
}
|
||||
|
||||
callback(null, { factory_name: factoryName, workplace_name: workplaceName });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 카테고리
|
||||
getAllCategories,
|
||||
@@ -910,6 +996,10 @@ module.exports = {
|
||||
|
||||
// System 3 연동
|
||||
updateMProjectId,
|
||||
getLocationNames,
|
||||
|
||||
// 유형 이관
|
||||
transferCategoryType,
|
||||
|
||||
// 상태 관리
|
||||
receiveReport,
|
||||
|
||||
@@ -69,6 +69,11 @@ router.put('/:id', workIssueController.updateReport);
|
||||
// 신고 삭제
|
||||
router.delete('/:id', workIssueController.deleteReport);
|
||||
|
||||
// ==================== 유형 이관 ====================
|
||||
|
||||
// 유형 이관 (인증 필요)
|
||||
router.put('/:id/transfer', workIssueController.transferReport);
|
||||
|
||||
// ==================== 상태 관리 ====================
|
||||
|
||||
// 신고 접수 (support_team 이상)
|
||||
|
||||
@@ -186,6 +186,7 @@ async function sendToMProject(issueData) {
|
||||
project_name,
|
||||
project_id = null,
|
||||
tk_issue_id,
|
||||
location_info = null,
|
||||
photos = [],
|
||||
ssoToken = null,
|
||||
} = issueData;
|
||||
@@ -207,13 +208,9 @@ async function sendToMProject(issueData) {
|
||||
// 카테고리 매핑
|
||||
const mProjectCategory = mapCategoryToMProject(category);
|
||||
|
||||
// 설명에 TK-FB 정보 추가
|
||||
// 설명에 신고자 정보 추가
|
||||
const enhancedDescription = [
|
||||
description,
|
||||
'',
|
||||
'---',
|
||||
`[TK-FB-Project 연동]`,
|
||||
`- 원본 이슈 ID: ${tk_issue_id}`,
|
||||
`- 신고자: ${reporter_name || '미상'}`,
|
||||
project_name ? `- 프로젝트: ${project_name}` : null,
|
||||
].filter(Boolean).join('\n');
|
||||
@@ -229,6 +226,10 @@ async function sendToMProject(issueData) {
|
||||
project_id: project_id || M_PROJECT_CONFIG.defaultProjectId,
|
||||
};
|
||||
|
||||
if (location_info) {
|
||||
requestBody.location_info = location_info;
|
||||
}
|
||||
|
||||
// 사진 추가
|
||||
base64Photos.forEach((photo, index) => {
|
||||
if (photo) {
|
||||
|
||||
Reference in New Issue
Block a user