// tbm.js - TBM 관리 페이지 JavaScript // 전역 변수 let allSessions = []; let todaySessions = []; let allWorkers = []; let allProjects = []; let allWorkTypes = []; let allTasks = []; let allSafetyChecks = []; let currentSessionId = null; let selectedWorkers = new Set(); let currentTab = 'tbm-input'; // 페이지 초기화 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 loadTodayOnlyTbm(); }); // 이벤트 리스너 설정 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 + '개'); } // 공정(Work Types) 목록 로드 const workTypesResponse = await window.apiCall('/tools/work-types'); if (workTypesResponse && workTypesResponse.success) { allWorkTypes = workTypesResponse.data || []; console.log('✅ 공정 목록 로드:', allWorkTypes.length + '개'); } // 작업(Tasks) 목록 로드 const tasksResponse = await window.apiCall('/tasks/active/list'); if (tasksResponse && tasksResponse.success) { allTasks = tasksResponse.data || []; console.log('✅ 작업 목록 로드:', allTasks.length + '개'); } } catch (error) { console.error('❌ 초기 데이터 로드 오류:', error); showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); } } // ==================== 탭 전환 ==================== // 탭 전환 function switchTbmTab(tabName) { currentTab = tabName; // 탭 버튼 활성화 상태 변경 document.querySelectorAll('.tab-btn').forEach(btn => { if (btn.dataset.tab === tabName) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // 탭 컨텐츠 표시 변경 document.querySelectorAll('.code-tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}-tab`).classList.add('active'); // 탭에 따라 데이터 로드 if (tabName === 'tbm-input') { loadTodayOnlyTbm(); } else if (tabName === 'tbm-manage') { const tbmDate = document.getElementById('tbmDate'); if (tbmDate && tbmDate.value) { loadTbmSessionsByDate(tbmDate.value); } else { loadTodayTbm(); } } } window.switchTbmTab = switchTbmTab; // ==================== TBM 입력 탭 ==================== // 오늘의 TBM만 로드 (TBM 입력 탭용) async function loadTodayOnlyTbm() { const today = new Date().toISOString().split('T')[0]; try { const response = await window.apiCall(`/tbm/sessions/date/${today}`); if (response && response.success) { todaySessions = response.data || []; displayTodayTbmSessions(); } else { todaySessions = []; displayTodayTbmSessions(); } } catch (error) { console.error('❌ 오늘 TBM 조회 오류:', error); showToast('오늘 TBM을 불러오는 중 오류가 발생했습니다.', 'error'); todaySessions = []; displayTodayTbmSessions(); } } window.loadTodayOnlyTbm = loadTodayOnlyTbm; // 오늘의 TBM 세션 표시 function displayTodayTbmSessions() { const grid = document.getElementById('todayTbmGrid'); const emptyState = document.getElementById('todayEmptyState'); const todayTotalEl = document.getElementById('todayTotalSessions'); const todayCompletedEl = document.getElementById('todayCompletedSessions'); const todayActiveEl = document.getElementById('todayActiveSessions'); if (todaySessions.length === 0) { grid.innerHTML = ''; emptyState.style.display = 'flex'; todayTotalEl.textContent = '0'; todayCompletedEl.textContent = '0'; todayActiveEl.textContent = '0'; return; } emptyState.style.display = 'none'; const completedCount = todaySessions.filter(s => s.status === 'completed').length; const activeCount = todaySessions.filter(s => s.status === 'draft').length; todayTotalEl.textContent = todaySessions.length; todayCompletedEl.textContent = completedCount; todayActiveEl.textContent = activeCount; grid.innerHTML = todaySessions.map(session => createSessionCard(session)).join(''); } // ==================== TBM 관리 탭 ==================== // 오늘 TBM 로드 (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 loadAllTbm() { try { const response = await window.apiCall('/tbm/sessions'); if (response && response.success) { allSessions = response.data || []; document.getElementById('tbmDate').value = ''; displayTbmSessions(); } else { allSessions = []; displayTbmSessions(); } } catch (error) { console.error('❌ 전체 TBM 조회 오류:', error); showToast('전체 TBM을 불러오는 중 오류가 발생했습니다.', 'error'); allSessions = []; displayTbmSessions(); } } window.loadAllTbm = loadAllTbm; // 특정 날짜의 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 => createSessionCard(session)).join(''); } // TBM 세션 카드 생성 (공통) function createSessionCard(session) { const statusBadge = { 'draft': '진행중', 'completed': '완료', 'cancelled': '취소' }[session.status] || ''; return `
${session.session_date} | ${session.leader_job_type || ''}
작업자를 선택해주세요
'; } 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(); // 목록 새로고침 if (currentTab === 'tbm-input') { await loadTodayOnlyTbm(); } else { 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 => `등록된 팀원이 없습니다.
'; } else { teamMembers.innerHTML = team.map(member => `안전 체크 기록이 없습니다.
'; } else { // 카테고리별 그룹화 const grouped = {}; safety.forEach(check => { if (!grouped[check.check_category]) { grouped[check.check_category] = []; } grouped[check.check_category].push(check); }); const categoryNames = { 'PPE': '개인 보호 장비', 'EQUIPMENT': '장비 점검', 'ENVIRONMENT': '작업 환경', 'EMERGENCY': '비상 대응' }; safetyChecks.innerHTML = Object.keys(grouped).map(category => `팀 구성이 없습니다.
'; } else { handoverTeamList.innerHTML = team.map(member => ` `).join(''); } // 기본값 설정 document.getElementById('handoverSessionId').value = sessionId; const today = new Date().toISOString().split('T')[0]; const now = new Date().toTimeString().slice(0, 5); document.getElementById('handoverDate').value = today; document.getElementById('handoverTime').value = now; document.getElementById('handoverReason').value = ''; document.getElementById('handoverNotes').value = ''; document.getElementById('handoverModal').style.display = 'flex'; document.body.style.overflow = 'hidden'; } catch (error) { console.error('❌ 인계 모달 열기 오류:', error); showToast('인계 정보를 불러오는 중 오류가 발생했습니다.', 'error'); } } window.openHandoverModal = openHandoverModal; // 인계 모달 닫기 function closeHandoverModal() { document.getElementById('handoverModal').style.display = 'none'; document.body.style.overflow = 'auto'; } window.closeHandoverModal = closeHandoverModal; // 작업 인계 저장 async function saveHandover() { const sessionId = currentSessionId; const toLeaderId = parseInt(document.getElementById('toLeaderId').value); const reason = document.getElementById('handoverReason').value; const handoverDate = document.getElementById('handoverDate').value; const handoverTime = document.getElementById('handoverTime').value; const handoverNotes = document.getElementById('handoverNotes').value; if (!toLeaderId || !reason || !handoverDate) { showToast('필수 항목을 입력해주세요.', 'error'); return; } // 인계할 작업자 목록 const workerIds = []; document.querySelectorAll('.handover-worker-checkbox:checked').forEach(cb => { workerIds.push(parseInt(cb.value)); }); if (workerIds.length === 0) { showToast('인계할 팀원을 최소 1명 이상 선택해주세요.', 'error'); return; } try { // 세션 정보 조회 (from_leader_id 가져오기) const sessionRes = await window.apiCall(`/tbm/sessions/${sessionId}`); const fromLeaderId = sessionRes?.data?.leader_id; if (!fromLeaderId) { showToast('세션 정보를 찾을 수 없습니다.', 'error'); return; } const handoverData = { session_id: sessionId, from_leader_id: fromLeaderId, to_leader_id: toLeaderId, handover_date: handoverDate, handover_time: handoverTime, reason: reason, handover_notes: handoverNotes, worker_ids: workerIds }; const response = await window.apiCall('/tbm/handovers', 'POST', handoverData); if (response && response.success) { showToast('작업 인계가 요청되었습니다.', 'success'); closeHandoverModal(); } else { throw new Error(response.message || '인계 요청에 실패했습니다.'); } } catch (error) { console.error('❌ 작업 인계 저장 오류:', error); showToast('작업 인계 중 오류가 발생했습니다.', 'error'); } } window.saveHandover = saveHandover; // 토스트 알림 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 = ` `; 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); }