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>
This commit is contained in:
@@ -698,6 +698,296 @@ const TbmModel = {
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ========== 안전 체크리스트 확장 메서드 ==========
|
||||
|
||||
/**
|
||||
* 유형별 안전 체크 항목 조회
|
||||
* @param {string} checkType - 체크 유형 (basic, weather, task)
|
||||
* @param {Object} options - 추가 옵션 (weatherCondition, taskId)
|
||||
*/
|
||||
getSafetyChecksByType: async (checkType, options = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let sql = `
|
||||
SELECT sc.*,
|
||||
wc.condition_name as weather_condition_name,
|
||||
wc.icon as weather_icon,
|
||||
t.task_name
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
WHERE sc.is_active = 1 AND sc.check_type = ?
|
||||
`;
|
||||
const params = [checkType];
|
||||
|
||||
if (checkType === 'weather' && options.weatherCondition) {
|
||||
sql += ' AND sc.weather_condition = ?';
|
||||
params.push(options.weatherCondition);
|
||||
}
|
||||
|
||||
if (checkType === 'task' && options.taskId) {
|
||||
sql += ' AND sc.task_id = ?';
|
||||
params.push(options.taskId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY sc.check_category, sc.display_order';
|
||||
|
||||
const [rows] = await db.query(sql, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 날씨 조건별 안전 체크 항목 조회 (복수 조건)
|
||||
* @param {string[]} conditions - 날씨 조건 배열 ['rain', 'wind']
|
||||
*/
|
||||
getSafetyChecksByWeather: async (conditions, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
const placeholders = conditions.map(() => '?').join(',');
|
||||
const sql = `
|
||||
SELECT sc.*,
|
||||
wc.condition_name as weather_condition_name,
|
||||
wc.icon as weather_icon
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'weather'
|
||||
AND sc.weather_condition IN (${placeholders})
|
||||
ORDER BY sc.weather_condition, sc.display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, conditions);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업별 안전 체크 항목 조회 (복수 작업)
|
||||
* @param {number[]} taskIds - 작업 ID 배열
|
||||
*/
|
||||
getSafetyChecksByTasks: async (taskIds, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!taskIds || taskIds.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
const placeholders = taskIds.map(() => '?').join(',');
|
||||
const sql = `
|
||||
SELECT sc.*,
|
||||
t.task_name,
|
||||
wt.name as work_type_name
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'task'
|
||||
AND sc.task_id IN (${placeholders})
|
||||
ORDER BY sc.task_id, sc.display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, taskIds);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션에 맞는 필터링된 안전 체크 항목 조회
|
||||
* 기본 + 날씨 + 작업별 체크항목 통합 조회
|
||||
* @param {number} sessionId - TBM 세션 ID
|
||||
* @param {string[]} weatherConditions - 날씨 조건 배열 (optional)
|
||||
*/
|
||||
getFilteredSafetyChecks: async (sessionId, weatherConditions = [], callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 세션 정보에서 작업 ID 목록 조회
|
||||
const [assignments] = await db.query(`
|
||||
SELECT DISTINCT task_id
|
||||
FROM tbm_team_assignments
|
||||
WHERE session_id = ? AND task_id IS NOT NULL
|
||||
`, [sessionId]);
|
||||
|
||||
const taskIds = assignments.map(a => a.task_id);
|
||||
|
||||
// 2. 기본 체크항목 조회
|
||||
const [basicChecks] = await db.query(`
|
||||
SELECT sc.*, 'basic' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
WHERE sc.is_active = 1 AND sc.check_type = 'basic'
|
||||
ORDER BY sc.check_category, sc.display_order
|
||||
`);
|
||||
|
||||
// 3. 날씨별 체크항목 조회
|
||||
let weatherChecks = [];
|
||||
if (weatherConditions && weatherConditions.length > 0) {
|
||||
const wcPlaceholders = weatherConditions.map(() => '?').join(',');
|
||||
const [rows] = await db.query(`
|
||||
SELECT sc.*, wc.condition_name as weather_condition_name, wc.icon as weather_icon,
|
||||
'weather' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'weather'
|
||||
AND sc.weather_condition IN (${wcPlaceholders})
|
||||
ORDER BY sc.weather_condition, sc.display_order
|
||||
`, weatherConditions);
|
||||
weatherChecks = rows;
|
||||
}
|
||||
|
||||
// 4. 작업별 체크항목 조회
|
||||
let taskChecks = [];
|
||||
if (taskIds.length > 0) {
|
||||
const taskPlaceholders = taskIds.map(() => '?').join(',');
|
||||
const [rows] = await db.query(`
|
||||
SELECT sc.*, t.task_name, wt.name as work_type_name,
|
||||
'task' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'task'
|
||||
AND sc.task_id IN (${taskPlaceholders})
|
||||
ORDER BY sc.task_id, sc.display_order
|
||||
`, taskIds);
|
||||
taskChecks = rows;
|
||||
}
|
||||
|
||||
// 5. 기존 체크 기록 조회
|
||||
const [existingRecords] = await db.query(`
|
||||
SELECT check_id, is_checked, notes
|
||||
FROM tbm_safety_records
|
||||
WHERE session_id = ?
|
||||
`, [sessionId]);
|
||||
|
||||
const recordMap = {};
|
||||
existingRecords.forEach(r => {
|
||||
recordMap[r.check_id] = { is_checked: r.is_checked, notes: r.notes };
|
||||
});
|
||||
|
||||
// 6. 기록과 병합
|
||||
const mergeWithRecords = (checks) => {
|
||||
return checks.map(check => ({
|
||||
...check,
|
||||
is_checked: recordMap[check.check_id]?.is_checked || false,
|
||||
notes: recordMap[check.check_id]?.notes || null
|
||||
}));
|
||||
};
|
||||
|
||||
const result = {
|
||||
basic: mergeWithRecords(basicChecks),
|
||||
weather: mergeWithRecords(weatherChecks),
|
||||
task: mergeWithRecords(taskChecks),
|
||||
totalCount: basicChecks.length + weatherChecks.length + taskChecks.length,
|
||||
weatherConditions: weatherConditions
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 생성 (관리자용)
|
||||
*/
|
||||
createSafetyCheck: async (checkData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_checks
|
||||
(check_category, check_type, weather_condition, task_id, check_item, description, is_required, display_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
checkData.check_category,
|
||||
checkData.check_type || 'basic',
|
||||
checkData.weather_condition || null,
|
||||
checkData.task_id || null,
|
||||
checkData.check_item,
|
||||
checkData.description || null,
|
||||
checkData.is_required !== false,
|
||||
checkData.display_order || 0
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, { insertId: result.insertId });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 수정 (관리자용)
|
||||
*/
|
||||
updateSafetyCheck: async (checkId, checkData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
UPDATE tbm_safety_checks
|
||||
SET check_category = ?,
|
||||
check_type = ?,
|
||||
weather_condition = ?,
|
||||
task_id = ?,
|
||||
check_item = ?,
|
||||
description = ?,
|
||||
is_required = ?,
|
||||
display_order = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE check_id = ?
|
||||
`;
|
||||
|
||||
const values = [
|
||||
checkData.check_category,
|
||||
checkData.check_type || 'basic',
|
||||
checkData.weather_condition || null,
|
||||
checkData.task_id || null,
|
||||
checkData.check_item,
|
||||
checkData.description || null,
|
||||
checkData.is_required !== false,
|
||||
checkData.display_order || 0,
|
||||
checkData.is_active !== false,
|
||||
checkId
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, { affectedRows: result.affectedRows });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 삭제 (비활성화)
|
||||
*/
|
||||
deleteSafetyCheck: async (checkId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
// 실제 삭제 대신 비활성화
|
||||
const sql = `UPDATE tbm_safety_checks SET is_active = 0 WHERE check_id = ?`;
|
||||
|
||||
const [result] = await db.query(sql, [checkId]);
|
||||
callback(null, { affectedRows: result.affectedRows });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user