refactor: TBM/작업보고 코드 통합 및 API 쿼리 버그 수정
- 공통 유틸리티 추출 (common/utils.js, common/base-state.js) - TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css) - 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일) - TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환 - 작업보고서 SSO 인증 호환 수정 (token/user 함수) - tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가 - docker-compose.yml: system1-web 볼륨 마운트 추가 - 모바일 인계(handover) 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
933
system2-report/api/controllers/workIssueModel.js
Normal file
933
system2-report/api/controllers/workIssueModel.js
Normal file
@@ -0,0 +1,933 @@
|
||||
/**
|
||||
* 작업 중 문제 신고 모델
|
||||
* 부적합/안전 신고 관련 DB 쿼리
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 신고 카테고리 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 신고 카테고리 조회
|
||||
*/
|
||||
const getAllCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_type, category_name, description, display_order, is_active, created_at
|
||||
FROM issue_report_categories
|
||||
ORDER BY category_type, display_order, category_id`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 타입별 활성 카테고리 조회 (nonconformity/safety)
|
||||
*/
|
||||
const getCategoriesByType = async (categoryType, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_type, category_name, description, display_order
|
||||
FROM issue_report_categories
|
||||
WHERE category_type = ? AND is_active = TRUE
|
||||
ORDER BY display_order, category_id`,
|
||||
[categoryType]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
const createCategory = async (categoryData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_type, category_name, description = null, display_order = 0 } = categoryData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO issue_report_categories (category_type, category_name, description, display_order)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[category_type, category_name, description, display_order]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 수정
|
||||
*/
|
||||
const updateCategory = async (categoryId, categoryData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_name, description, display_order, is_active } = categoryData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE issue_report_categories
|
||||
SET category_name = ?, description = ?, display_order = ?, is_active = ?
|
||||
WHERE category_id = ?`,
|
||||
[category_name, description, display_order, is_active, categoryId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
const deleteCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM issue_report_categories WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 사전 정의 신고 항목 관리 ====================
|
||||
|
||||
/**
|
||||
* 카테고리별 활성 항목 조회
|
||||
*/
|
||||
const getItemsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT item_id, category_id, item_name, description, severity, display_order
|
||||
FROM issue_report_items
|
||||
WHERE category_id = ? AND is_active = TRUE
|
||||
ORDER BY display_order, item_id`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 항목 조회 (관리용)
|
||||
*/
|
||||
const getAllItems = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT iri.item_id, iri.category_id, iri.item_name, iri.description,
|
||||
iri.severity, iri.display_order, iri.is_active, iri.created_at,
|
||||
irc.category_name, irc.category_type
|
||||
FROM issue_report_items iri
|
||||
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 생성
|
||||
*/
|
||||
const createItem = async (itemData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[category_id, item_name, description, severity, display_order]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 수정
|
||||
*/
|
||||
const updateItem = async (itemId, itemData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { item_name, description, severity, display_order, is_active } = itemData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE issue_report_items
|
||||
SET item_name = ?, description = ?, severity = ?, display_order = ?, is_active = ?
|
||||
WHERE item_id = ?`,
|
||||
[item_name, description, severity, display_order, is_active, itemId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 삭제
|
||||
*/
|
||||
const deleteItem = async (itemId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM issue_report_items WHERE item_id = ?`,
|
||||
[itemId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 ID로 단건 조회
|
||||
*/
|
||||
const getCategoryById = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_type, category_name, description
|
||||
FROM issue_report_categories
|
||||
WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows[0] || null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 문제 신고 관리 ====================
|
||||
|
||||
// 한국 시간 유틸리티 import
|
||||
const { getKoreaDatetime } = require('../utils/dateUtils');
|
||||
|
||||
/**
|
||||
* 신고 생성
|
||||
*/
|
||||
const createReport = async (reportData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
reporter_id,
|
||||
factory_category_id = null,
|
||||
workplace_id = null,
|
||||
project_id = null,
|
||||
custom_location = null,
|
||||
tbm_session_id = null,
|
||||
visit_request_id = null,
|
||||
issue_category_id,
|
||||
issue_item_id = null,
|
||||
additional_description = null,
|
||||
photo_path1 = null,
|
||||
photo_path2 = null,
|
||||
photo_path3 = null,
|
||||
photo_path4 = null,
|
||||
photo_path5 = null
|
||||
} = reportData;
|
||||
|
||||
// 한국 시간 기준으로 신고 일시 설정
|
||||
const reportDate = getKoreaDatetime();
|
||||
|
||||
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,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5)
|
||||
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,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5]
|
||||
);
|
||||
|
||||
// 상태 변경 로그 기록
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, NULL, 'reported', ?)`,
|
||||
[result.insertId, reporter_id]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 목록 조회 (필터 옵션 포함)
|
||||
*/
|
||||
const getAllReports = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
wir.report_id, wir.reporter_id, wir.report_date,
|
||||
wir.factory_category_id, wir.workplace_id, wir.custom_location,
|
||||
wir.tbm_session_id, wir.visit_request_id,
|
||||
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
|
||||
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
|
||||
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at,
|
||||
wir.resolution_notes, wir.resolved_at,
|
||||
wir.created_at, wir.updated_at,
|
||||
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,
|
||||
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
|
||||
INNER JOIN users u ON wir.reporter_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
|
||||
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 필터 적용
|
||||
if (filters.status) {
|
||||
query += ` AND wir.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
if (filters.category_type) {
|
||||
query += ` AND irc.category_type = ?`;
|
||||
params.push(filters.category_type);
|
||||
}
|
||||
|
||||
if (filters.issue_category_id) {
|
||||
query += ` AND wir.issue_category_id = ?`;
|
||||
params.push(filters.issue_category_id);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
query += ` AND wir.factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
if (filters.workplace_id) {
|
||||
query += ` AND wir.workplace_id = ?`;
|
||||
params.push(filters.workplace_id);
|
||||
}
|
||||
|
||||
if (filters.reporter_id) {
|
||||
query += ` AND wir.reporter_id = ?`;
|
||||
params.push(filters.reporter_id);
|
||||
}
|
||||
|
||||
if (filters.assigned_user_id) {
|
||||
query += ` AND wir.assigned_user_id = ?`;
|
||||
params.push(filters.assigned_user_id);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
query += ` AND (wir.additional_description LIKE ? OR iri.item_name LIKE ? OR wir.custom_location LIKE ?)`;
|
||||
const searchTerm = `%${filters.search}%`;
|
||||
params.push(searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
query += ` ORDER BY wir.report_date DESC, wir.report_id DESC`;
|
||||
|
||||
// 페이지네이션
|
||||
if (filters.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(parseInt(filters.limit));
|
||||
|
||||
if (filters.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(parseInt(filters.offset));
|
||||
}
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 상세 조회
|
||||
*/
|
||||
const getReportById = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
wir.report_id, wir.reporter_id, wir.report_date,
|
||||
wir.factory_category_id, wir.workplace_id, wir.custom_location,
|
||||
wir.tbm_session_id, wir.visit_request_id,
|
||||
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
|
||||
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
|
||||
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at, wir.assigned_by,
|
||||
wir.resolution_notes, wir.resolution_photo_path1, wir.resolution_photo_path2,
|
||||
wir.resolved_at, wir.resolved_by,
|
||||
wir.modification_history,
|
||||
wir.created_at, wir.updated_at,
|
||||
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,
|
||||
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,
|
||||
resolver.username as resolved_by_name
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN users u ON wir.reporter_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
|
||||
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
|
||||
LEFT JOIN users assigner ON wir.assigned_by = assigner.user_id
|
||||
LEFT JOIN users resolver ON wir.resolved_by = resolver.user_id
|
||||
WHERE wir.report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 수정
|
||||
*/
|
||||
const updateReport = async (reportId, reportData, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 데이터 조회
|
||||
const [existing] = await db.query(
|
||||
`SELECT * FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
const current = existing[0];
|
||||
|
||||
// 수정 이력 생성
|
||||
const modifications = [];
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const key of Object.keys(reportData)) {
|
||||
if (current[key] !== reportData[key] && reportData[key] !== undefined) {
|
||||
modifications.push({
|
||||
field: key,
|
||||
old_value: current[key],
|
||||
new_value: reportData[key],
|
||||
modified_at: now,
|
||||
modified_by: userId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 이력과 병합
|
||||
const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : [];
|
||||
const newHistory = [...existingHistory, ...modifications];
|
||||
|
||||
const {
|
||||
factory_category_id,
|
||||
workplace_id,
|
||||
custom_location,
|
||||
issue_category_id,
|
||||
issue_item_id,
|
||||
additional_description,
|
||||
photo_path1,
|
||||
photo_path2,
|
||||
photo_path3,
|
||||
photo_path4,
|
||||
photo_path5
|
||||
} = reportData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET factory_category_id = COALESCE(?, factory_category_id),
|
||||
workplace_id = COALESCE(?, workplace_id),
|
||||
custom_location = COALESCE(?, custom_location),
|
||||
issue_category_id = COALESCE(?, issue_category_id),
|
||||
issue_item_id = COALESCE(?, issue_item_id),
|
||||
additional_description = COALESCE(?, additional_description),
|
||||
photo_path1 = COALESCE(?, photo_path1),
|
||||
photo_path2 = COALESCE(?, photo_path2),
|
||||
photo_path3 = COALESCE(?, photo_path3),
|
||||
photo_path4 = COALESCE(?, photo_path4),
|
||||
photo_path5 = COALESCE(?, photo_path5),
|
||||
modification_history = ?,
|
||||
updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[factory_category_id, workplace_id, custom_location,
|
||||
issue_category_id, issue_item_id, additional_description,
|
||||
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
|
||||
JSON.stringify(newHistory), reportId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 삭제
|
||||
*/
|
||||
const deleteReport = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 사진 경로 조회 (삭제용)
|
||||
const [photos] = await db.query(
|
||||
`SELECT photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
|
||||
resolution_photo_path1, resolution_photo_path2
|
||||
FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 삭제할 사진 경로 반환
|
||||
callback(null, { result, photos: photos[0] });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 상태 관리 ====================
|
||||
|
||||
/**
|
||||
* 신고 접수 (reported → received)
|
||||
*/
|
||||
const receiveReport = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'reported') {
|
||||
return callback(new Error('접수 대기 상태가 아닙니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'received', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'reported', 'received', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 담당자 배정
|
||||
*/
|
||||
const assignReport = async (reportId, assignData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { assigned_department, assigned_user_id, assigned_by } = assignData;
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
// 접수 상태 이상이어야 배정 가능
|
||||
const validStatuses = ['received', 'in_progress'];
|
||||
if (!validStatuses.includes(current[0].status)) {
|
||||
return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET assigned_department = ?, assigned_user_id = ?,
|
||||
assigned_at = NOW(), assigned_by = ?, updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[assigned_department, assigned_user_id, assigned_by, reportId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 시작 (received → in_progress)
|
||||
*/
|
||||
const startProcessing = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'received') {
|
||||
return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'in_progress', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'received', 'in_progress', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 완료 (in_progress → completed)
|
||||
*/
|
||||
const completeReport = async (reportId, completionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData;
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'in_progress') {
|
||||
return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'completed', resolution_notes = ?,
|
||||
resolution_photo_path1 = ?, resolution_photo_path2 = ?,
|
||||
resolved_at = NOW(), resolved_by = ?, updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by, reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by, change_reason)
|
||||
VALUES (?, 'in_progress', 'completed', ?, ?)`,
|
||||
[reportId, resolved_by, resolution_notes]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 종료 (completed → closed)
|
||||
*/
|
||||
const closeReport = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'completed') {
|
||||
return callback(new Error('완료된 상태에서만 종료할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'closed', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'completed', 'closed', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 조회
|
||||
*/
|
||||
const getStatusLogs = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status,
|
||||
wisl.changed_by, wisl.change_reason, wisl.changed_at,
|
||||
u.username as changed_by_name, u.name as changed_by_full_name
|
||||
FROM work_issue_status_logs wisl
|
||||
INNER JOIN users u ON wisl.changed_by = u.user_id
|
||||
WHERE wisl.report_id = ?
|
||||
ORDER BY wisl.changed_at ASC`,
|
||||
[reportId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* m_project_id 업데이트 (System 3 연동 후)
|
||||
*/
|
||||
const updateMProjectId = async (reportId, mProjectId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
`UPDATE work_issue_reports SET m_project_id = ? WHERE report_id = ?`,
|
||||
[mProjectId, reportId]
|
||||
);
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 통계 ====================
|
||||
|
||||
/**
|
||||
* 신고 통계 요약
|
||||
*/
|
||||
const getStatsSummary = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
let joinClause = '';
|
||||
|
||||
if (filters.category_type) {
|
||||
joinClause = ' INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id';
|
||||
whereClause += ` AND irc.category_type = ?`;
|
||||
params.push(filters.category_type);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.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 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${joinClause}
|
||||
WHERE ${whereClause}`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리별 통계
|
||||
*/
|
||||
const getStatsByCategory = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
irc.category_type, irc.category_name,
|
||||
COUNT(*) as count
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY irc.category_id
|
||||
ORDER BY irc.category_type, count DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장별 통계
|
||||
*/
|
||||
const getStatsByWorkplace = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = 'wir.workplace_id IS NOT NULL';
|
||||
const params = [];
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
whereClause += ` AND wir.factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
wir.factory_category_id, wc.category_name as factory_name,
|
||||
wir.workplace_id, w.workplace_name,
|
||||
COUNT(*) as count
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY wir.factory_category_id, wir.workplace_id
|
||||
ORDER BY count DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 카테고리
|
||||
getAllCategories,
|
||||
getCategoriesByType,
|
||||
getCategoryById,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
|
||||
// 항목
|
||||
getItemsByCategory,
|
||||
getAllItems,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
|
||||
// 신고
|
||||
createReport,
|
||||
getAllReports,
|
||||
getReportById,
|
||||
updateReport,
|
||||
deleteReport,
|
||||
|
||||
// System 3 연동
|
||||
updateMProjectId,
|
||||
|
||||
// 상태 관리
|
||||
receiveReport,
|
||||
assignReport,
|
||||
startProcessing,
|
||||
completeReport,
|
||||
closeReport,
|
||||
getStatusLogs,
|
||||
|
||||
// 통계
|
||||
getStatsSummary,
|
||||
getStatsByCategory,
|
||||
getStatsByWorkplace
|
||||
};
|
||||
Reference in New Issue
Block a user