diff --git a/web-ui/js/tbm.js b/web-ui/js/tbm.js new file mode 100644 index 0000000..9c018ad --- /dev/null +++ b/web-ui/js/tbm.js @@ -0,0 +1,638 @@ +// tbm.js - TBM 관리 페이지 JavaScript + +// 전역 변수 +let allSessions = []; +let allWorkers = []; +let allProjects = []; +let allSafetyChecks = []; +let currentSessionId = null; +let selectedWorkers = new Set(); + +// 페이지 초기화 +document.addEventListener('DOMContentLoaded', async () => { + console.log('🛠️ TBM 관리 페이지 초기화'); + + // API 함수가 로드될 때까지 대기 + let retryCount = 0; + while (!window.apiCall && retryCount < 50) { + await new Promise(resolve => setTimeout(resolve, 100)); + retryCount++; + } + + if (!window.apiCall) { + showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error'); + return; + } + + // 오늘 날짜 설정 + const today = new Date().toISOString().split('T')[0]; + document.getElementById('tbmDate').value = today; + document.getElementById('sessionDate').value = today; + + // 이벤트 리스너 설정 + setupEventListeners(); + + // 초기 데이터 로드 + await loadInitialData(); + await loadTodayTbm(); +}); + +// 이벤트 리스너 설정 +function setupEventListeners() { + const tbmDateInput = document.getElementById('tbmDate'); + if (tbmDateInput) { + tbmDateInput.addEventListener('change', () => { + const date = tbmDateInput.value; + loadTbmSessionsByDate(date); + }); + } +} + +// 초기 데이터 로드 +async function loadInitialData() { + try { + // 작업자 목록 로드 + const workersResponse = await window.apiCall('/workers?limit=1000'); + if (workersResponse) { + allWorkers = Array.isArray(workersResponse) ? workersResponse : (workersResponse.data || []); + // 활성 상태인 작업자만 필터링 + allWorkers = allWorkers.filter(w => w.status === 'active' && w.employment_status === 'employed'); + console.log('✅ 작업자 목록 로드:', allWorkers.length + '명'); + } + + // 프로젝트 목록 로드 + const projectsResponse = await window.apiCall('/projects?is_active=1'); + if (projectsResponse) { + allProjects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []); + console.log('✅ 프로젝트 목록 로드:', allProjects.length + '개'); + populateProjectSelect(); + } + + // 안전 체크리스트 로드 + const safetyResponse = await window.apiCall('/tbm/safety-checks'); + if (safetyResponse && safetyResponse.success) { + allSafetyChecks = safetyResponse.data; + console.log('✅ 안전 체크리스트 로드:', allSafetyChecks.length + '개'); + } + + } catch (error) { + console.error('❌ 초기 데이터 로드 오류:', error); + showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); + } +} + +// 오늘 TBM 로드 +async function loadTodayTbm() { + const today = new Date().toISOString().split('T')[0]; + document.getElementById('tbmDate').value = today; + await loadTbmSessionsByDate(today); +} +window.loadTodayTbm = loadTodayTbm; + +// 특정 날짜의 TBM 세션 목록 로드 +async function loadTbmSessionsByDate(date) { + try { + const response = await window.apiCall(`/tbm/sessions/date/${date}`); + + if (response && response.success) { + allSessions = response.data || []; + displayTbmSessions(); + } else { + allSessions = []; + displayTbmSessions(); + } + } catch (error) { + console.error('❌ TBM 세션 조회 오류:', error); + showToast('TBM 세션을 불러오는 중 오류가 발생했습니다.', 'error'); + allSessions = []; + displayTbmSessions(); + } +} + +// TBM 세션 목록 표시 +function displayTbmSessions() { + const grid = document.getElementById('tbmSessionsGrid'); + const emptyState = document.getElementById('emptyState'); + const totalSessionsEl = document.getElementById('totalSessions'); + const completedSessionsEl = document.getElementById('completedSessions'); + + if (allSessions.length === 0) { + grid.innerHTML = ''; + emptyState.style.display = 'flex'; + totalSessionsEl.textContent = '0'; + completedSessionsEl.textContent = '0'; + return; + } + + emptyState.style.display = 'none'; + + const completedCount = allSessions.filter(s => s.status === 'completed').length; + totalSessionsEl.textContent = allSessions.length; + completedSessionsEl.textContent = completedCount; + + grid.innerHTML = allSessions.map(session => { + const statusBadge = { + 'draft': '진행중', + 'completed': '완료', + 'cancelled': '취소' + }[session.status] || ''; + + return ` +
+
+
+

+ ${session.leader_name || '팀장 미지정'} +

+

+ ${session.leader_job_type || ''} +

+
+ ${statusBadge} +
+ +
+
+ 프로젝트 + ${session.project_name || '-'} +
+
+ 작업 장소 + ${session.work_location || '-'} +
+
+ 팀원 수 + ${session.team_member_count || 0}명 +
+
+ 시작 시간 + ${session.start_time || '-'} +
+
+ + ${session.work_description ? ` +
+ ${session.work_description} +
+ ` : ''} + +
+ ${session.status === 'draft' ? ` + + + + ` : ''} +
+
+ `; + }).join(''); +} + +// 새 TBM 모달 열기 +function openNewTbmModal() { + currentSessionId = null; + document.getElementById('modalTitle').textContent = '새 TBM 시작'; + document.getElementById('sessionId').value = ''; + document.getElementById('tbmForm').reset(); + + const today = new Date().toISOString().split('T')[0]; + document.getElementById('sessionDate').value = today; + + // 팀장 목록 로드 + populateLeaderSelect(); + populateProjectSelect(); + + document.getElementById('tbmModal').style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} +window.openNewTbmModal = openNewTbmModal; + +// 팀장 선택 드롭다운 채우기 +function populateLeaderSelect() { + const leaderSelect = document.getElementById('leaderId'); + if (!leaderSelect) return; + + const leaders = allWorkers.filter(w => + w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin' + ); + + leaderSelect.innerHTML = '' + + leaders.map(w => ` + + `).join(''); +} + +// 프로젝트 선택 드롭다운 채우기 +function populateProjectSelect() { + const projectSelect = document.getElementById('projectId'); + if (!projectSelect) return; + + projectSelect.innerHTML = '' + + allProjects.map(p => ` + + `).join(''); +} + +// TBM 모달 닫기 +function closeTbmModal() { + document.getElementById('tbmModal').style.display = 'none'; + document.body.style.overflow = 'auto'; +} +window.closeTbmModal = closeTbmModal; + +// TBM 세션 저장 +async function saveTbmSession() { + const sessionData = { + session_date: document.getElementById('sessionDate').value, + leader_id: parseInt(document.getElementById('leaderId').value), + project_id: document.getElementById('projectId').value || null, + work_location: document.getElementById('workLocation').value || null, + work_description: document.getElementById('workDescription').value || null, + safety_notes: document.getElementById('safetyNotes').value || null, + start_time: document.getElementById('startTime').value || null + }; + + if (!sessionData.session_date || !sessionData.leader_id) { + showToast('TBM 날짜와 팀장을 선택해주세요.', 'error'); + return; + } + + try { + const response = await window.apiCall('/tbm/sessions', 'POST', sessionData); + + if (response && response.success) { + showToast('TBM 세션이 생성되었습니다.', 'success'); + closeTbmModal(); + + const createdSessionId = response.data.session_id; + + // 목록 새로고침 + await loadTbmSessionsByDate(sessionData.session_date); + + // 팀 구성 모달 열기 + setTimeout(() => { + openTeamCompositionModal(createdSessionId); + }, 500); + } else { + throw new Error(response.message || '저장에 실패했습니다.'); + } + } catch (error) { + console.error('❌ TBM 세션 저장 오류:', error); + showToast('TBM 세션 저장 중 오류가 발생했습니다.', 'error'); + } +} +window.saveTbmSession = saveTbmSession; + +// 팀 구성 모달 열기 +async function openTeamCompositionModal(sessionId) { + currentSessionId = sessionId; + selectedWorkers.clear(); + + // 기존 팀 구성 로드 + try { + const response = await window.apiCall(`/tbm/sessions/${sessionId}/team`); + if (response && response.success) { + response.data.forEach(member => { + selectedWorkers.add(member.worker_id); + }); + } + } catch (error) { + console.error('팀 구성 조회 오류:', error); + } + + // 작업자 선택 그리드 생성 + const grid = document.getElementById('workerSelectionGrid'); + grid.innerHTML = allWorkers.map(worker => ` + + `).join(''); + + updateSelectedWorkers(); + + document.getElementById('teamModal').style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} +window.openTeamCompositionModal = openTeamCompositionModal; + +// 선택된 작업자 업데이트 +function updateSelectedWorkers() { + selectedWorkers.clear(); + + document.querySelectorAll('.worker-checkbox:checked').forEach(cb => { + selectedWorkers.add(parseInt(cb.dataset.workerId)); + }); + + const selectedCount = document.getElementById('selectedCount'); + const selectedList = document.getElementById('selectedWorkersList'); + + selectedCount.textContent = selectedWorkers.size; + + if (selectedWorkers.size === 0) { + selectedList.innerHTML = '

작업자를 선택해주세요

'; + } else { + const selectedWorkersArray = Array.from(selectedWorkers).map(id => { + const worker = allWorkers.find(w => w.worker_id === id); + return worker ? ` + + ${worker.worker_name} + + + ` : ''; + }); + selectedList.innerHTML = selectedWorkersArray.join(''); + } +} +window.updateSelectedWorkers = updateSelectedWorkers; + +// 작업자 제거 +function removeWorker(workerId) { + const checkbox = document.querySelector(`.worker-checkbox[data-worker-id="${workerId}"]`); + if (checkbox) { + checkbox.checked = false; + updateSelectedWorkers(); + } +} +window.removeWorker = removeWorker; + +// 전체 선택 +function selectAllWorkers() { + document.querySelectorAll('.worker-checkbox').forEach(cb => { + cb.checked = true; + }); + updateSelectedWorkers(); +} +window.selectAllWorkers = selectAllWorkers; + +// 전체 해제 +function deselectAllWorkers() { + document.querySelectorAll('.worker-checkbox').forEach(cb => { + cb.checked = false; + }); + updateSelectedWorkers(); +} +window.deselectAllWorkers = deselectAllWorkers; + +// 팀 구성 모달 닫기 +function closeTeamModal() { + document.getElementById('teamModal').style.display = 'none'; + document.body.style.overflow = 'auto'; +} +window.closeTeamModal = closeTeamModal; + +// 팀 구성 저장 +async function saveTeamComposition() { + if (selectedWorkers.size === 0) { + showToast('최소 1명 이상의 작업자를 선택해주세요.', 'error'); + return; + } + + const members = Array.from(selectedWorkers).map(workerId => ({ + worker_id: workerId + })); + + try { + const response = await window.apiCall( + `/tbm/sessions/${currentSessionId}/team/batch`, + 'POST', + { members } + ); + + if (response && response.success) { + showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success'); + closeTeamModal(); + + // 목록 새로고침 + const date = document.getElementById('tbmDate').value; + await loadTbmSessionsByDate(date); + } else { + throw new Error(response.message || '저장에 실패했습니다.'); + } + } catch (error) { + console.error('❌ 팀 구성 저장 오류:', error); + showToast('팀 구성 저장 중 오류가 발생했습니다.', 'error'); + } +} +window.saveTeamComposition = saveTeamComposition; + +// 안전 체크 모달 열기 +async function openSafetyCheckModal(sessionId) { + currentSessionId = sessionId; + + // 기존 안전 체크 기록 로드 + try { + const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety`); + const existingRecords = response && response.success ? response.data : []; + + // 카테고리별로 그룹화 + const grouped = {}; + allSafetyChecks.forEach(check => { + if (!grouped[check.check_category]) { + grouped[check.check_category] = []; + } + + const existingRecord = existingRecords.find(r => r.check_id === check.check_id); + grouped[check.check_category].push({ + ...check, + is_checked: existingRecord ? existingRecord.is_checked : false, + notes: existingRecord ? existingRecord.notes : '' + }); + }); + + const categoryNames = { + 'PPE': '개인 보호 장비', + 'EQUIPMENT': '장비 점검', + 'ENVIRONMENT': '작업 환경', + 'EMERGENCY': '비상 대응' + }; + + const container = document.getElementById('safetyChecklistContainer'); + container.innerHTML = Object.keys(grouped).map(category => ` +
+
+ ${categoryNames[category] || category} +
+ ${grouped[category].map(check => ` +
+ +
+ `).join('')} +
+ `).join(''); + + document.getElementById('safetyModal').style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + } catch (error) { + console.error('❌ 안전 체크 조회 오류:', error); + showToast('안전 체크 정보를 불러오는 중 오류가 발생했습니다.', 'error'); + } +} +window.openSafetyCheckModal = openSafetyCheckModal; + +// 안전 체크 모달 닫기 +function closeSafetyModal() { + document.getElementById('safetyModal').style.display = 'none'; + document.body.style.overflow = 'auto'; +} +window.closeSafetyModal = closeSafetyModal; + +// 안전 체크리스트 저장 +async function saveSafetyChecklist() { + const records = []; + + document.querySelectorAll('.safety-check').forEach(cb => { + records.push({ + check_id: parseInt(cb.dataset.checkId), + is_checked: cb.checked + }); + }); + + try { + const response = await window.apiCall( + `/tbm/sessions/${currentSessionId}/safety`, + 'POST', + { records } + ); + + if (response && response.success) { + showToast('안전 체크가 완료되었습니다.', 'success'); + closeSafetyModal(); + } else { + throw new Error(response.message || '저장에 실패했습니다.'); + } + } catch (error) { + console.error('❌ 안전 체크 저장 오류:', error); + showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error'); + } +} +window.saveSafetyChecklist = saveSafetyChecklist; + +// TBM 완료 모달 열기 +function openCompleteTbmModal(sessionId) { + currentSessionId = sessionId; + const now = new Date(); + const timeString = now.toTimeString().slice(0, 5); + document.getElementById('endTime').value = timeString; + + document.getElementById('completeModal').style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} +window.openCompleteTbmModal = openCompleteTbmModal; + +// 완료 모달 닫기 +function closeCompleteModal() { + document.getElementById('completeModal').style.display = 'none'; + document.body.style.overflow = 'auto'; +} +window.closeCompleteModal = closeCompleteModal; + +// TBM 세션 완료 +async function completeTbmSession() { + const endTime = document.getElementById('endTime').value; + + try { + const response = await window.apiCall( + `/tbm/sessions/${currentSessionId}/complete`, + 'POST', + { end_time: endTime } + ); + + if (response && response.success) { + showToast('TBM이 완료되었습니다.', 'success'); + closeCompleteModal(); + + // 목록 새로고침 + const date = document.getElementById('tbmDate').value; + await loadTbmSessionsByDate(date); + } else { + throw new Error(response.message || '완료 처리에 실패했습니다.'); + } + } catch (error) { + console.error('❌ TBM 완료 처리 오류:', error); + showToast('TBM 완료 처리 중 오류가 발생했습니다.', 'error'); + } +} +window.completeTbmSession = completeTbmSession; + +// TBM 세션 상세 보기 +function viewTbmSession(sessionId) { + // TODO: 상세 보기 페이지 또는 모달 구현 + console.log('TBM 세션 상세 보기:', sessionId); +} +window.viewTbmSession = viewTbmSession; + +// 토스트 알림 +function showToast(message, type = 'info', duration = 3000) { + const container = document.getElementById('toastContainer'); + if (!container) return; + + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + + const iconMap = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️' + }; + + toast.innerHTML = ` +
${iconMap[type] || 'ℹ️'}
+
${message}
+ + `; + + toast.style.cssText = ` + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + background: white; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); + margin-bottom: 0.75rem; + min-width: 300px; + animation: slideIn 0.3s ease-out; + `; + + container.appendChild(toast); + + setTimeout(() => { + if (toast.parentElement) { + toast.style.animation = 'slideOut 0.3s ease-out'; + setTimeout(() => toast.remove(), 300); + } + }, duration); +}