Files
TK-FB-Project/web-ui/js/safety-checklist-manage.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

719 lines
20 KiB
JavaScript

/**
* 안전 체크리스트 관리 페이지 스크립트
*
* 3가지 유형의 체크리스트 항목을 관리:
* 1. 기본 사항 - 항상 표시
* 2. 날씨별 - 날씨 조건에 따라 표시
* 3. 작업별 - 선택한 작업에 따라 표시
*
* @since 2026-02-02
*/
import { apiCall } from './api-config.js';
// 전역 상태
let allChecks = [];
let weatherConditions = [];
let workTypes = [];
let tasks = [];
let currentTab = 'basic';
let editingCheckId = null;
// 카테고리 정보
const CATEGORIES = {
PPE: { name: 'PPE (개인보호장비)', icon: '🦺' },
EQUIPMENT: { name: 'EQUIPMENT (장비점검)', icon: '🔧' },
ENVIRONMENT: { name: 'ENVIRONMENT (작업환경)', icon: '🏗️' },
EMERGENCY: { name: 'EMERGENCY (비상대응)', icon: '🚨' },
WEATHER: { name: 'WEATHER (날씨)', icon: '🌤️' },
TASK: { name: 'TASK (작업)', icon: '📋' }
};
// 날씨 아이콘 매핑
const WEATHER_ICONS = {
clear: '☀️',
rain: '🌧️',
snow: '❄️',
heat: '🔥',
cold: '🥶',
wind: '💨',
fog: '🌫️',
dust: '😷'
};
/**
* 페이지 초기화
*/
async function initPage() {
try {
console.log('📋 안전 체크리스트 관리 페이지 초기화...');
await Promise.all([
loadAllChecks(),
loadWeatherConditions(),
loadWorkTypes()
]);
renderCurrentTab();
console.log('✅ 초기화 완료. 체크항목:', allChecks.length, '개');
} catch (error) {
console.error('초기화 실패:', error);
showToast('데이터를 불러오는데 실패했습니다.', 'error');
}
}
// DOMContentLoaded 이벤트
document.addEventListener('DOMContentLoaded', initPage);
/**
* 모든 안전 체크 항목 로드
*/
async function loadAllChecks() {
try {
const response = await apiCall('/tbm/safety-checks');
if (response && response.success) {
allChecks = response.data || [];
console.log('✅ 체크 항목 로드:', allChecks.length, '개');
} else {
console.warn('체크 항목 응답 실패:', response);
allChecks = [];
}
} catch (error) {
console.error('체크 항목 로드 실패:', error);
allChecks = [];
}
}
/**
* 날씨 조건 목록 로드
*/
async function loadWeatherConditions() {
try {
const response = await apiCall('/tbm/weather/conditions');
if (response && response.success) {
weatherConditions = response.data || [];
populateWeatherSelects();
console.log('✅ 날씨 조건 로드:', weatherConditions.length, '개');
}
} catch (error) {
console.error('날씨 조건 로드 실패:', error);
weatherConditions = [];
}
}
/**
* 공정(작업 유형) 목록 로드
*/
async function loadWorkTypes() {
try {
const response = await apiCall('/daily-work-reports/work-types');
if (response && response.success) {
workTypes = response.data || [];
populateWorkTypeSelects();
console.log('✅ 공정 목록 로드:', workTypes.length, '개');
}
} catch (error) {
console.error('공정 목록 로드 실패:', error);
workTypes = [];
}
}
/**
* 날씨 조건 셀렉트 박스 채우기
*/
function populateWeatherSelects() {
const filterSelect = document.getElementById('weatherFilter');
const modalSelect = document.getElementById('weatherCondition');
const options = weatherConditions.map(wc =>
`<option value="${wc.condition_code}">${WEATHER_ICONS[wc.condition_code] || ''} ${wc.condition_name}</option>`
).join('');
if (filterSelect) {
filterSelect.innerHTML = `<option value="">모든 날씨 조건</option>${options}`;
}
if (modalSelect) {
modalSelect.innerHTML = options || '<option value="">날씨 조건 없음</option>';
}
}
/**
* 공정 셀렉트 박스 채우기
*/
function populateWorkTypeSelects() {
const filterSelect = document.getElementById('workTypeFilter');
const modalSelect = document.getElementById('modalWorkType');
const options = workTypes.map(wt =>
`<option value="${wt.work_type_id}">${wt.work_type_name}</option>`
).join('');
if (filterSelect) {
filterSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
}
if (modalSelect) {
modalSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
}
}
/**
* 탭 전환
*/
function switchTab(tabName) {
currentTab = tabName;
// 탭 버튼 상태 업데이트
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// 탭 콘텐츠 표시/숨김
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}Tab`);
});
renderCurrentTab();
}
/**
* 현재 탭 렌더링
*/
function renderCurrentTab() {
switch (currentTab) {
case 'basic':
renderBasicChecks();
break;
case 'weather':
renderWeatherChecks();
break;
case 'task':
renderTaskChecks();
break;
}
}
/**
* 기본 체크 항목 렌더링
*/
function renderBasicChecks() {
const container = document.getElementById('basicChecklistContainer');
const basicChecks = allChecks.filter(c => c.check_type === 'basic');
console.log('기본 체크항목:', basicChecks.length, '개');
if (basicChecks.length === 0) {
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.');
return;
}
// 카테고리별로 그룹화
const grouped = groupByCategory(basicChecks);
container.innerHTML = Object.entries(grouped).map(([category, items]) =>
renderChecklistGroup(category, items)
).join('');
}
/**
* 날씨별 체크 항목 렌더링
*/
function renderWeatherChecks() {
const container = document.getElementById('weatherChecklistContainer');
const filterValue = document.getElementById('weatherFilter')?.value;
let weatherChecks = allChecks.filter(c => c.check_type === 'weather');
if (filterValue) {
weatherChecks = weatherChecks.filter(c => c.weather_condition === filterValue);
}
if (weatherChecks.length === 0) {
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.');
return;
}
// 날씨 조건별로 그룹화
const grouped = groupByWeather(weatherChecks);
container.innerHTML = Object.entries(grouped).map(([condition, items]) => {
const conditionInfo = weatherConditions.find(wc => wc.condition_code === condition);
const icon = WEATHER_ICONS[condition] || '🌤️';
const name = conditionInfo?.condition_name || condition;
return renderChecklistGroup(`${icon} ${name}`, items, condition);
}).join('');
}
/**
* 작업별 체크 항목 렌더링
*/
function renderTaskChecks() {
const container = document.getElementById('taskChecklistContainer');
const workTypeId = document.getElementById('workTypeFilter')?.value;
const taskId = document.getElementById('taskFilter')?.value;
let taskChecks = allChecks.filter(c => c.check_type === 'task');
if (taskId) {
taskChecks = taskChecks.filter(c => c.task_id == taskId);
} else if (workTypeId && tasks.length > 0) {
const workTypeTasks = tasks.filter(t => t.work_type_id == workTypeId);
const taskIds = workTypeTasks.map(t => t.task_id);
taskChecks = taskChecks.filter(c => taskIds.includes(c.task_id));
}
if (taskChecks.length === 0) {
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.');
return;
}
// 작업별로 그룹화
const grouped = groupByTask(taskChecks);
container.innerHTML = Object.entries(grouped).map(([taskId, items]) => {
const task = tasks.find(t => t.task_id == taskId);
const taskName = task?.task_name || `작업 ${taskId}`;
return renderChecklistGroup(`📋 ${taskName}`, items, null, taskId);
}).join('');
}
/**
* 카테고리별 그룹화
*/
function groupByCategory(checks) {
return checks.reduce((acc, check) => {
const category = check.check_category || 'OTHER';
if (!acc[category]) acc[category] = [];
acc[category].push(check);
return acc;
}, {});
}
/**
* 날씨 조건별 그룹화
*/
function groupByWeather(checks) {
return checks.reduce((acc, check) => {
const condition = check.weather_condition || 'other';
if (!acc[condition]) acc[condition] = [];
acc[condition].push(check);
return acc;
}, {});
}
/**
* 작업별 그룹화
*/
function groupByTask(checks) {
return checks.reduce((acc, check) => {
const taskId = check.task_id || 0;
if (!acc[taskId]) acc[taskId] = [];
acc[taskId].push(check);
return acc;
}, {});
}
/**
* 체크리스트 그룹 렌더링
*/
function renderChecklistGroup(title, items, weatherCondition = null, taskId = null) {
const categoryInfo = CATEGORIES[title] || { name: title, icon: '' };
const displayTitle = categoryInfo.name !== title ? categoryInfo.name : title;
const icon = categoryInfo.icon || '';
// 표시 순서로 정렬
items.sort((a, b) => (a.display_order || 0) - (b.display_order || 0));
return `
<div class="checklist-group">
<div class="group-header">
<div class="group-title">
<span class="group-icon">${icon}</span>
<span>${displayTitle}</span>
</div>
<span class="group-count">${items.length}개</span>
</div>
<div class="checklist-items">
${items.map(item => renderChecklistItem(item)).join('')}
</div>
</div>
`;
}
/**
* 체크리스트 항목 렌더링
*/
function renderChecklistItem(item) {
const requiredBadge = item.is_required
? '<span class="item-badge badge-required">필수</span>'
: '<span class="item-badge badge-optional">선택</span>';
return `
<div class="checklist-item" data-check-id="${item.check_id}">
<div class="item-info">
<div class="item-name">${item.check_item}</div>
<div class="item-meta">
${requiredBadge}
${item.description ? `<span>${item.description}</span>` : ''}
</div>
</div>
<div class="item-actions">
<button class="btn-icon btn-edit" onclick="openEditModal(${item.check_id})" title="수정">
✏️
</button>
<button class="btn-icon btn-delete" onclick="confirmDelete(${item.check_id})" title="삭제">
🗑️
</button>
</div>
</div>
`;
}
/**
* 빈 상태 렌더링
*/
function renderEmptyState(message) {
return `
<div class="empty-state">
<div class="empty-state-icon">📋</div>
<p>${message}</p>
</div>
`;
}
/**
* 날씨 필터 변경
*/
function filterByWeather() {
renderWeatherChecks();
}
/**
* 공정 필터 변경
*/
async function filterByWorkType() {
const workTypeId = document.getElementById('workTypeFilter')?.value;
const taskSelect = document.getElementById('taskFilter');
// workTypeId가 없거나 빈 문자열이면 early return
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
if (taskSelect) {
taskSelect.innerHTML = '<option value="">작업 선택</option>';
}
tasks = [];
renderTaskChecks();
return;
}
try {
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
if (response && response.success) {
tasks = response.data || [];
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
tasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
}
} catch (error) {
console.error('작업 목록 로드 실패:', error);
tasks = [];
}
renderTaskChecks();
}
/**
* 작업 필터 변경
*/
function filterByTask() {
renderTaskChecks();
}
/**
* 모달의 작업 목록 로드
*/
async function loadModalTasks() {
const workTypeId = document.getElementById('modalWorkType')?.value;
const taskSelect = document.getElementById('modalTask');
// workTypeId가 없거나 빈 문자열이면 early return
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
if (taskSelect) {
taskSelect.innerHTML = '<option value="">작업 선택</option>';
}
return;
}
try {
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
if (response && response.success) {
const modalTasks = response.data || [];
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
modalTasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
}
} catch (error) {
console.error('작업 목록 로드 실패:', error);
}
}
/**
* 조건부 필드 토글
*/
function toggleConditionalFields() {
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
document.getElementById('basicFields').classList.toggle('show', checkType === 'basic');
document.getElementById('weatherFields').classList.toggle('show', checkType === 'weather');
document.getElementById('taskFields').classList.toggle('show', checkType === 'task');
}
/**
* 추가 모달 열기
*/
function openAddModal() {
editingCheckId = null;
document.getElementById('modalTitle').textContent = '체크 항목 추가';
// 폼 초기화
document.getElementById('checkForm').reset();
document.getElementById('checkId').value = '';
// 현재 탭에 맞는 유형 선택
const typeRadio = document.querySelector(`input[name="checkType"][value="${currentTab}"]`);
if (typeRadio) {
typeRadio.checked = true;
}
toggleConditionalFields();
showModal();
}
/**
* 수정 모달 열기
*/
async function openEditModal(checkId) {
editingCheckId = checkId;
const check = allChecks.find(c => c.check_id === checkId);
if (!check) {
showToast('항목을 찾을 수 없습니다.', 'error');
return;
}
document.getElementById('modalTitle').textContent = '체크 항목 수정';
document.getElementById('checkId').value = checkId;
// 유형 선택
const typeRadio = document.querySelector(`input[name="checkType"][value="${check.check_type}"]`);
if (typeRadio) {
typeRadio.checked = true;
}
toggleConditionalFields();
// 카테고리
if (check.check_type === 'basic') {
document.getElementById('checkCategory').value = check.check_category || 'PPE';
}
// 날씨 조건
if (check.check_type === 'weather') {
document.getElementById('weatherCondition').value = check.weather_condition || '';
}
// 작업
if (check.check_type === 'task' && check.task_id) {
// 먼저 공정 찾기 (task를 통해)
const task = tasks.find(t => t.task_id === check.task_id);
if (task) {
document.getElementById('modalWorkType').value = task.work_type_id;
await loadModalTasks();
document.getElementById('modalTask').value = check.task_id;
}
}
// 공통 필드
document.getElementById('checkItem').value = check.check_item || '';
document.getElementById('checkDescription').value = check.description || '';
document.getElementById('isRequired').checked = check.is_required === 1 || check.is_required === true;
document.getElementById('displayOrder').value = check.display_order || 0;
showModal();
}
/**
* 모달 표시
*/
function showModal() {
document.getElementById('checkModal').style.display = 'flex';
}
/**
* 모달 닫기
*/
function closeModal() {
document.getElementById('checkModal').style.display = 'none';
editingCheckId = null;
}
/**
* 체크 항목 저장
*/
async function saveCheck() {
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
const checkItem = document.getElementById('checkItem').value.trim();
if (!checkItem) {
showToast('체크 항목을 입력해주세요.', 'error');
return;
}
const data = {
check_type: checkType,
check_item: checkItem,
description: document.getElementById('checkDescription').value.trim() || null,
is_required: document.getElementById('isRequired').checked,
display_order: parseInt(document.getElementById('displayOrder').value) || 0
};
// 유형별 추가 데이터
switch (checkType) {
case 'basic':
data.check_category = document.getElementById('checkCategory').value;
break;
case 'weather':
data.check_category = 'WEATHER';
data.weather_condition = document.getElementById('weatherCondition').value;
if (!data.weather_condition) {
showToast('날씨 조건을 선택해주세요.', 'error');
return;
}
break;
case 'task':
data.check_category = 'TASK';
data.task_id = document.getElementById('modalTask').value;
if (!data.task_id) {
showToast('작업을 선택해주세요.', 'error');
return;
}
break;
}
try {
let response;
if (editingCheckId) {
// 수정
response = await apiCall(`/tbm/safety-checks/${editingCheckId}`, 'PUT', data);
} else {
// 추가
response = await apiCall('/tbm/safety-checks', 'POST', data);
}
if (response && response.success) {
showToast(editingCheckId ? '항목이 수정되었습니다.' : '항목이 추가되었습니다.', 'success');
closeModal();
await loadAllChecks();
renderCurrentTab();
} else {
showToast(response?.message || '저장에 실패했습니다.', 'error');
}
} catch (error) {
console.error('저장 실패:', error);
showToast('저장 중 오류가 발생했습니다.', 'error');
}
}
/**
* 삭제 확인
*/
function confirmDelete(checkId) {
const check = allChecks.find(c => c.check_id === checkId);
if (!check) {
showToast('항목을 찾을 수 없습니다.', 'error');
return;
}
if (confirm(`"${check.check_item}" 항목을 삭제하시겠습니까?`)) {
deleteCheck(checkId);
}
}
/**
* 체크 항목 삭제
*/
async function deleteCheck(checkId) {
try {
const response = await apiCall(`/tbm/safety-checks/${checkId}`, 'DELETE');
if (response && response.success) {
showToast('항목이 삭제되었습니다.', 'success');
await loadAllChecks();
renderCurrentTab();
} else {
showToast(response?.message || '삭제에 실패했습니다.', 'error');
}
} catch (error) {
console.error('삭제 실패:', error);
showToast('삭제 중 오류가 발생했습니다.', 'error');
}
}
/**
* 토스트 메시지 표시
*/
function showToast(message, type = 'info') {
// 기존 토스트 제거
const existingToast = document.querySelector('.toast-message');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `toast-message toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 9999;
animation: fadeInUp 0.3s ease;
${type === 'success' ? 'background: #10b981; color: white;' : ''}
${type === 'error' ? 'background: #ef4444; color: white;' : ''}
${type === 'info' ? 'background: #3b82f6; color: white;' : ''}
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 모달 외부 클릭 시 닫기
document.getElementById('checkModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// HTML onclick에서 호출할 수 있도록 전역에 노출
window.switchTab = switchTab;
window.openAddModal = openAddModal;
window.openEditModal = openEditModal;
window.closeModal = closeModal;
window.saveCheck = saveCheck;
window.confirmDelete = confirmDelete;
window.filterByWeather = filterByWeather;
window.filterByWorkType = filterByWorkType;
window.filterByTask = filterByTask;
window.loadModalTasks = loadModalTasks;
window.toggleConditionalFields = toggleConditionalFields;