// daily-work-report.js - 브라우저 호환 버전 // ================================================================= // 🌐 API 설정 (window 객체에서 가져오기) // ================================================================= // API 설정은 api-config.js에서 window 객체에 설정됨 // 전역 변수 let workTypes = []; let workStatusTypes = []; let errorTypes = []; let workers = []; let projects = []; let selectedWorkers = new Set(); let workEntryCounter = 0; let currentStep = 1; let editingWorkId = null; // 수정 중인 작업 ID let incompleteTbms = []; // 미완료 TBM 작업 목록 let currentTab = 'tbm'; // 현재 활성 탭 // 작업장소 지도 관련 변수 let mapCanvas = null; let mapCtx = null; let mapImage = null; let mapRegions = []; let selectedWorkplace = null; let selectedWorkplaceName = null; let selectedWorkplaceCategory = null; let selectedWorkplaceCategoryName = null; // 시간 선택 관련 변수 let currentEditingField = null; // { index, type: 'total' | 'error' } let currentTimeValue = 0; // ================================================================= // TBM 작업보고 관련 함수 // ================================================================= /** * 탭 전환 함수 */ window.switchTab = function(tab) { currentTab = tab; const tbmBtn = document.getElementById('tbmReportTab'); const completedBtn = document.getElementById('completedReportTab'); const tbmSection = document.getElementById('tbmReportSection'); const completedSection = document.getElementById('completedReportSection'); // 모든 탭 버튼 비활성화 tbmBtn.classList.remove('active'); completedBtn.classList.remove('active'); // 모든 섹션 숨기기 tbmSection.style.display = 'none'; completedSection.style.display = 'none'; // 선택된 탭 활성화 if (tab === 'tbm') { tbmBtn.classList.add('active'); tbmSection.style.display = 'block'; loadIncompleteTbms(); // TBM 목록 로드 } else if (tab === 'completed') { completedBtn.classList.add('active'); completedSection.style.display = 'block'; // 오늘 날짜로 초기화 document.getElementById('completedReportDate').value = getKoreaToday(); loadCompletedReports(); } }; /** * 미완료 TBM 작업 로드 */ async function loadIncompleteTbms() { try { const response = await window.apiCall('/tbm/sessions/incomplete-reports'); if (!response.success) { throw new Error(response.message || '미완료 TBM 조회 실패'); } let data = response.data || []; // 사용자 권한 확인 및 필터링 const user = getUser(); if (user && user.role !== 'Admin' && user.access_level !== 'system') { // 일반 사용자: 자신이 생성한 세션만 표시 const userId = user.user_id; data = data.filter(tbm => tbm.created_by === userId); } // 관리자는 모든 데이터 표시 incompleteTbms = data; renderTbmWorkList(); } catch (error) { console.error('미완료 TBM 로드 오류:', error); showMessage('TBM 작업 목록을 불러오는 중 오류가 발생했습니다.', 'error'); } } /** * 사용자 정보 가져오기 (auth-check.js와 동일한 로직) */ function getUser() { const user = localStorage.getItem('user'); return user ? JSON.parse(user) : null; } /** * TBM 작업 목록 렌더링 (세션별 그룹화) */ function renderTbmWorkList() { const container = document.getElementById('tbmWorkList'); // TBM을 세션별로 그룹화 const groupedTbms = {}; if (incompleteTbms && incompleteTbms.length > 0) { incompleteTbms.forEach((tbm, index) => { const key = `${tbm.session_id}_${tbm.session_date}`; if (!groupedTbms[key]) { groupedTbms[key] = { session_id: tbm.session_id, session_date: tbm.session_date, created_by_name: tbm.created_by_name, items: [] }; } groupedTbms[key].items.push({ ...tbm, originalIndex: index }); }); } let html = `

작업보고서 목록

`; // 수동 입력 섹션 먼저 추가 (맨 위) html += `
수동 입력 TBM에 없는 작업을 추가로 입력할 수 있습니다
작업자 날짜 프로젝트 공정 작업 작업장소 작업시간
(시간)
부적합
(시간)
부적합 원인 제출
`; // 각 TBM 세션별로 테이블 생성 Object.keys(groupedTbms).forEach(key => { const group = groupedTbms[key]; html += `
TBM 세션 ${formatDate(group.session_date)} 작성자: ${group.created_by_name} ${group.items.length}명
${group.items.map(tbm => { const index = tbm.originalIndex; return ` `; }).join('')}
작업자 프로젝트 공정 작업 작업장소 작업시간
(시간)
부적합
(시간)
부적합 원인 제출
${tbm.worker_name || '작업자'}
${tbm.job_type || '-'}
${tbm.project_name || '-'} ${tbm.work_type_name || '-'} ${tbm.task_name || '-'}
${tbm.category_name || ''}
${tbm.workplace_name || '-'}
시간 선택
0시간
-
`; }); container.innerHTML = html; } /** * 부적합 시간 입력 처리 */ window.calculateRegularHours = function(index) { const errorInput = document.getElementById(`errorHours_${index}`); const errorTypeSelect = document.getElementById(`errorType_${index}`); const errorTypeNone = document.getElementById(`errorTypeNone_${index}`); const errorHours = parseFloat(errorInput.value) || 0; // 부적합 시간이 있으면 원인 선택 표시 if (errorHours > 0) { errorTypeSelect.style.display = 'inline-block'; if (errorTypeNone) errorTypeNone.style.display = 'none'; } else { errorTypeSelect.style.display = 'none'; if (errorTypeNone) errorTypeNone.style.display = 'inline'; } }; /** * TBM 작업보고서 제출 */ window.submitTbmWorkReport = async function(index) { const tbm = incompleteTbms[index]; const totalHours = parseFloat(document.getElementById(`totalHours_${index}`).value); const errorHours = parseFloat(document.getElementById(`errorHours_${index}`).value) || 0; const errorTypeId = document.getElementById(`errorType_${index}`).value; // 필수 필드 검증 if (!totalHours || totalHours <= 0) { showMessage('작업시간을 입력해주세요.', 'error'); return; } if (errorHours > totalHours) { showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error'); return; } if (errorHours > 0 && !errorTypeId) { showMessage('부적합 처리 시간이 있는 경우 원인을 선택해주세요.', 'error'); return; } // 날짜를 YYYY-MM-DD 형식으로 변환 const reportDate = tbm.session_date instanceof Date ? tbm.session_date.toISOString().split('T')[0] : (typeof tbm.session_date === 'string' && tbm.session_date.includes('T') ? tbm.session_date.split('T')[0] : tbm.session_date); const reportData = { tbm_assignment_id: tbm.assignment_id, tbm_session_id: tbm.session_id, worker_id: tbm.worker_id, project_id: tbm.project_id, work_type_id: tbm.work_type_id, report_date: reportDate, start_time: null, end_time: null, total_hours: totalHours, error_hours: errorHours, error_type_id: errorTypeId || null, work_status_id: errorHours > 0 ? 2 : 1 }; console.log('🔍 TBM 제출 데이터:', JSON.stringify(reportData, null, 2)); console.log('🔍 tbm 객체:', tbm); try { const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData); if (!response.success) { throw new Error(response.message || '작업보고서 제출 실패'); } showSaveResultModal( 'success', '작업보고서 제출 완료', `${tbm.worker_name}의 작업보고서가 성공적으로 제출되었습니다.`, response.data.tbm_completed ? '모든 팀원의 작업보고서가 제출되어 TBM이 완료되었습니다.' : response.data.completion_status ); // 목록 새로고침 await loadIncompleteTbms(); } catch (error) { console.error('TBM 작업보고서 제출 오류:', error); showSaveResultModal('error', '제출 실패', error.message); } }; /** * TBM 세션 일괄제출 */ window.batchSubmitTbmSession = async function(sessionKey) { // 해당 세션의 모든 항목 가져오기 const sessionRows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"]`); if (sessionRows.length === 0) { showMessage('제출할 항목이 없습니다.', 'error'); return; } // 1단계: 모든 항목 검증 const validationErrors = []; const itemsToSubmit = []; sessionRows.forEach((row, rowIndex) => { const index = parseInt(row.getAttribute('data-index')); const tbm = incompleteTbms[index]; const totalHours = parseFloat(document.getElementById(`totalHours_${index}`)?.value); const errorHours = parseFloat(document.getElementById(`errorHours_${index}`)?.value) || 0; const errorTypeId = document.getElementById(`errorType_${index}`)?.value; // 검증 if (!totalHours || totalHours <= 0) { validationErrors.push(`${tbm.worker_name}: 작업시간 미입력`); return; } if (errorHours > totalHours) { validationErrors.push(`${tbm.worker_name}: 부적합 시간이 총 작업시간 초과`); return; } if (errorHours > 0 && !errorTypeId) { validationErrors.push(`${tbm.worker_name}: 부적합 원인 미선택`); return; } // 검증 통과한 항목 저장 const reportDate = tbm.session_date instanceof Date ? tbm.session_date.toISOString().split('T')[0] : (typeof tbm.session_date === 'string' && tbm.session_date.includes('T') ? tbm.session_date.split('T')[0] : tbm.session_date); itemsToSubmit.push({ index, tbm, data: { tbm_assignment_id: tbm.assignment_id, tbm_session_id: tbm.session_id, worker_id: tbm.worker_id, project_id: tbm.project_id, work_type_id: tbm.work_type_id, report_date: reportDate, start_time: null, end_time: null, total_hours: totalHours, error_hours: errorHours, error_type_id: errorTypeId || null, work_status_id: errorHours > 0 ? 2 : 1 } }); }); // 검증 실패가 하나라도 있으면 전체 중단 if (validationErrors.length > 0) { showSaveResultModal( 'error', '일괄제출 검증 실패', '모든 항목이 유효해야 제출할 수 있습니다.', validationErrors ); return; } // 2단계: 모든 항목 제출 const submitBtn = event.target; submitBtn.disabled = true; submitBtn.textContent = '제출 중...'; const results = { success: [], failed: [] }; try { for (const item of itemsToSubmit) { try { const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', item.data); if (response.success) { results.success.push(item.tbm.worker_name); } else { results.failed.push(`${item.tbm.worker_name}: ${response.message}`); } } catch (error) { results.failed.push(`${item.tbm.worker_name}: ${error.message}`); } } // 결과 표시 const totalCount = itemsToSubmit.length; const successCount = results.success.length; const failedCount = results.failed.length; if (failedCount === 0) { // 모두 성공 showSaveResultModal( 'success', '일괄제출 완료', `${totalCount}건의 작업보고서가 모두 성공적으로 제출되었습니다.`, results.success.map(name => `✓ ${name}`) ); } else if (successCount === 0) { // 모두 실패 showSaveResultModal( 'error', '일괄제출 실패', `${totalCount}건의 작업보고서가 모두 실패했습니다.`, results.failed.map(msg => `✗ ${msg}`) ); } else { // 일부 성공, 일부 실패 const details = [ ...results.success.map(name => `✓ ${name} - 성공`), ...results.failed.map(msg => `✗ ${msg}`) ]; showSaveResultModal( 'warning', '일괄제출 부분 완료', `성공: ${successCount}건 / 실패: ${failedCount}건`, details ); } // 목록 새로고침 await loadIncompleteTbms(); } catch (error) { console.error('일괄제출 오류:', error); showSaveResultModal('error', '일괄제출 오류', error.message); } finally { submitBtn.disabled = false; submitBtn.textContent = `📤 이 세션 일괄제출 (${sessionRows.length}건)`; } }; /** * 수동 작업 추가 */ window.addManualWorkRow = function() { const tbody = document.getElementById('manualWorkTableBody'); if (!tbody) { showMessage('수동 입력 테이블을 찾을 수 없습니다.', 'error'); return; } const manualIndex = `manual_${workEntryCounter++}`; const newRow = document.createElement('tr'); newRow.setAttribute('data-index', manualIndex); newRow.setAttribute('data-type', 'manual'); newRow.innerHTML = `
🗺️ 작업장소
클릭하여 선택
시간 선택
0시간
- `; tbody.appendChild(newRow); showMessage('새 작업 행이 추가되었습니다. 정보를 입력하고 제출하세요.', 'info'); }; /** * 수동 작업 행 제거 */ window.removeManualWorkRow = function(manualIndex) { const row = document.querySelector(`tr[data-index="${manualIndex}"]`); if (row) { row.remove(); } }; /** * 공정 선택 시 작업 목록 로드 */ window.loadTasksForWorkType = async function(manualIndex) { const workTypeId = document.getElementById(`workType_${manualIndex}`).value; const taskSelect = document.getElementById(`task_${manualIndex}`); if (!workTypeId) { taskSelect.disabled = true; taskSelect.innerHTML = ''; return; } try { // 해당 공정의 작업 목록 조회 const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`); const tasks = response.success ? response.data : (Array.isArray(response) ? response : []); if (tasks && tasks.length > 0) { taskSelect.disabled = false; taskSelect.innerHTML = ` ${tasks.map(task => ``).join('')} `; } else { taskSelect.disabled = true; taskSelect.innerHTML = ''; } } catch (error) { console.error('작업 목록 로드 오류:', error); taskSelect.disabled = true; taskSelect.innerHTML = ''; } }; /** * 수동 입력 부적합 시간 토글 */ window.toggleManualErrorType = function(manualIndex) { const errorInput = document.getElementById(`errorHours_${manualIndex}`); const errorTypeSelect = document.getElementById(`errorType_${manualIndex}`); const errorTypeNone = document.getElementById(`errorTypeNone_${manualIndex}`); const errorHours = parseFloat(errorInput.value) || 0; if (errorHours > 0) { errorTypeSelect.style.display = 'inline-block'; if (errorTypeNone) errorTypeNone.style.display = 'none'; } else { errorTypeSelect.style.display = 'none'; if (errorTypeNone) errorTypeNone.style.display = 'inline'; } }; /** * 수동 입력용 작업장소 선택 모달 열기 */ window.openWorkplaceMapForManual = async function(manualIndex) { window.currentManualIndex = manualIndex; // 변수 초기화 selectedWorkplace = null; selectedWorkplaceName = null; selectedWorkplaceCategory = null; selectedWorkplaceCategoryName = null; try { // 작업장소 카테고리 로드 const categoriesResponse = await window.apiCall('/workplaces/categories'); const categories = categoriesResponse.success ? categoriesResponse.data : categoriesResponse; // 작업장소 모달 표시 const modal = document.getElementById('workplaceModal'); const categoryList = document.getElementById('workplaceCategoryList'); categoryList.innerHTML = categories.map(cat => ` `).join(''); // 카테고리 선택 화면 표시 document.getElementById('categorySelectionArea').style.display = 'block'; document.getElementById('workplaceSelectionArea').style.display = 'none'; modal.style.display = 'flex'; } catch (error) { console.error('작업장소 카테고리 로드 오류:', error); showMessage('작업장소 목록을 불러오는 중 오류가 발생했습니다.', 'error'); } }; /** * 작업장소 카테고리 선택 */ window.selectWorkplaceCategory = async function(categoryId, categoryName, layoutImage) { selectedWorkplaceCategory = categoryId; selectedWorkplaceCategoryName = categoryName; try { // 타이틀 업데이트 document.getElementById('selectedCategoryTitle').textContent = `${categoryName} - 작업장 선택`; // 카테고리 화면 숨기고 작업장 선택 화면 표시 document.getElementById('categorySelectionArea').style.display = 'none'; document.getElementById('workplaceSelectionArea').style.display = 'block'; // 해당 카테고리의 작업장소 로드 const workplacesResponse = await window.apiCall(`/workplaces?category_id=${categoryId}`); const workplaces = workplacesResponse.success ? workplacesResponse.data : workplacesResponse; // 지도 또는 리스트 로드 if (layoutImage && layoutImage !== '') { // 지도가 있는 경우 - 지도 영역 표시 await loadWorkplaceMap(categoryId, layoutImage, workplaces); document.getElementById('layoutMapArea').style.display = 'block'; } else { // 지도가 없는 경우 - 리스트만 표시 document.getElementById('layoutMapArea').style.display = 'none'; } // 리스트 항상 표시 const workplaceListArea = document.getElementById('workplaceListArea'); workplaceListArea.innerHTML = workplaces.map(wp => ` `).join(''); } catch (error) { console.error('작업장소 로드 오류:', error); showMessage('작업장소를 불러오는 중 오류가 발생했습니다.', 'error'); } }; /** * 작업장소 지도 로드 */ async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) { try { mapCanvas = document.getElementById('workplaceMapCanvas'); if (!mapCanvas) return; mapCtx = mapCanvas.getContext('2d'); // 이미지 URL 생성 const baseUrl = window.API_BASE_URL || 'http://localhost:20005'; const apiBaseUrl = baseUrl.replace('/api', ''); // /api 제거 const fullImageUrl = layoutImagePath.startsWith('http') ? layoutImagePath : `${apiBaseUrl}${layoutImagePath}`; console.log('🖼️ 이미지 로드 시도:', fullImageUrl); // 지도 영역 데이터 로드 const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`); if (regionsResponse && regionsResponse.success) { mapRegions = regionsResponse.data || []; } else { mapRegions = []; } // 이미지 로드 mapImage = new Image(); mapImage.crossOrigin = 'anonymous'; mapImage.onload = function() { // 캔버스 크기 설정 (최대 너비 800px) const maxWidth = 800; const scale = mapImage.width > maxWidth ? maxWidth / mapImage.width : 1; mapCanvas.width = mapImage.width * scale; mapCanvas.height = mapImage.height * scale; // 이미지와 영역 그리기 drawWorkplaceMap(); // 클릭 이벤트 리스너 추가 mapCanvas.onclick = handleMapClick; console.log(`✅ 작업장 지도 로드 완료: ${mapRegions.length}개 영역`); }; mapImage.onerror = function() { console.error('❌ 지도 이미지 로드 실패'); document.getElementById('layoutMapArea').style.display = 'none'; showMessage('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning'); }; mapImage.src = fullImageUrl; } catch (error) { console.error('❌ 작업장 지도 로드 오류:', error); document.getElementById('layoutMapArea').style.display = 'none'; } } /** * 지도 그리기 */ function drawWorkplaceMap() { if (!mapCanvas || !mapCtx || !mapImage) return; // 이미지 그리기 mapCtx.drawImage(mapImage, 0, 0, mapCanvas.width, mapCanvas.height); // 각 영역 그리기 mapRegions.forEach((region) => { // 퍼센트를 픽셀로 변환 const x1 = (region.x_start / 100) * mapCanvas.width; const y1 = (region.y_start / 100) * mapCanvas.height; const x2 = (region.x_end / 100) * mapCanvas.width; const y2 = (region.y_end / 100) * mapCanvas.height; const width = x2 - x1; const height = y2 - y1; // 선택된 영역인지 확인 const isSelected = region.workplace_id === selectedWorkplace; // 영역 테두리 mapCtx.strokeStyle = isSelected ? '#3b82f6' : '#10b981'; mapCtx.lineWidth = isSelected ? 4 : 2; mapCtx.strokeRect(x1, y1, width, height); // 영역 배경 (반투명) mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.25)' : 'rgba(16, 185, 129, 0.15)'; mapCtx.fillRect(x1, y1, width, height); // 작업장 이름 표시 if (region.workplace_name) { mapCtx.font = 'bold 14px sans-serif'; // 텍스트 배경 const textMetrics = mapCtx.measureText(region.workplace_name); const textPadding = 6; mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.95)' : 'rgba(16, 185, 129, 0.95)'; mapCtx.fillRect(x1 + 5, y1 + 5, textMetrics.width + textPadding * 2, 24); // 텍스트 mapCtx.fillStyle = '#ffffff'; mapCtx.fillText(region.workplace_name, x1 + 5 + textPadding, y1 + 22); } }); } /** * 지도 클릭 이벤트 처리 */ function handleMapClick(event) { if (!mapCanvas || mapRegions.length === 0) return; const rect = mapCanvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // 클릭한 위치에 있는 영역 찾기 for (let i = mapRegions.length - 1; i >= 0; i--) { const region = mapRegions[i]; const x1 = (region.x_start / 100) * mapCanvas.width; const y1 = (region.y_start / 100) * mapCanvas.height; const x2 = (region.x_end / 100) * mapCanvas.width; const y2 = (region.y_end / 100) * mapCanvas.height; if (x >= x1 && x <= x2 && y >= y1 && y <= y2) { // 영역 클릭됨 selectWorkplaceFromList(region.workplace_id, region.workplace_name); return; } } } /** * 리스트에서 작업장소 선택 */ window.selectWorkplaceFromList = function(workplaceId, workplaceName) { selectedWorkplace = workplaceId; selectedWorkplaceName = workplaceName; // 지도 다시 그리기 (선택 효과 표시) if (mapCanvas && mapCtx && mapImage) { drawWorkplaceMap(); } // 리스트 버튼 업데이트 document.querySelectorAll('[id^="workplace-"]').forEach(btn => { if (btn.id === `workplace-${workplaceId}`) { btn.classList.remove('btn-secondary'); btn.classList.add('btn-primary'); } else { btn.classList.remove('btn-primary'); btn.classList.add('btn-secondary'); } }); // 선택 완료 버튼 활성화 document.getElementById('confirmWorkplaceBtn').disabled = false; }; /** * 작업장소 선택 완료 */ window.confirmWorkplaceSelection = function() { const manualIndex = window.currentManualIndex; if (!selectedWorkplace || !selectedWorkplaceCategory) { showMessage('작업장소를 선택해주세요.', 'error'); return; } document.getElementById(`workplaceCategory_${manualIndex}`).value = selectedWorkplaceCategory; document.getElementById(`workplace_${manualIndex}`).value = selectedWorkplace; const displayDiv = document.getElementById(`workplaceDisplay_${manualIndex}`); if (displayDiv) { displayDiv.innerHTML = `
작업장소 선택됨
🏭 ${selectedWorkplaceCategoryName}
📍 ${selectedWorkplaceName}
`; displayDiv.style.background = '#ecfdf5'; displayDiv.style.borderColor = '#10b981'; } // 모달 닫기 closeWorkplaceModal(); showMessage('작업장소가 선택되었습니다.', 'success'); }; /** * 작업장소 모달 닫기 */ window.closeWorkplaceModal = function() { document.getElementById('workplaceModal').style.display = 'none'; // 초기화 selectedWorkplace = null; selectedWorkplaceName = null; mapCanvas = null; mapCtx = null; mapImage = null; mapRegions = []; }; /** * 수동 작업보고서 제출 */ window.submitManualWorkReport = async function(manualIndex) { const workerId = document.getElementById(`worker_${manualIndex}`).value; const reportDate = document.getElementById(`date_${manualIndex}`).value; const projectId = document.getElementById(`project_${manualIndex}`).value; const workTypeId = document.getElementById(`workType_${manualIndex}`).value; const taskId = document.getElementById(`task_${manualIndex}`).value; const workplaceCategoryId = document.getElementById(`workplaceCategory_${manualIndex}`).value; const workplaceId = document.getElementById(`workplace_${manualIndex}`).value; const totalHours = parseFloat(document.getElementById(`totalHours_${manualIndex}`).value); const errorHours = parseFloat(document.getElementById(`errorHours_${manualIndex}`).value) || 0; const errorTypeId = document.getElementById(`errorType_${manualIndex}`).value; // 필수 필드 검증 if (!workerId) { showMessage('작업자를 선택해주세요.', 'error'); return; } if (!reportDate) { showMessage('작업 날짜를 입력해주세요.', 'error'); return; } if (!projectId) { showMessage('프로젝트를 선택해주세요.', 'error'); return; } if (!workTypeId) { showMessage('공정을 선택해주세요.', 'error'); return; } if (!taskId) { showMessage('작업을 선택해주세요.', 'error'); return; } if (!workplaceId) { showMessage('작업장소를 선택해주세요.', 'error'); return; } if (!totalHours || totalHours <= 0) { showMessage('작업시간을 입력해주세요.', 'error'); return; } if (errorHours > totalHours) { showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error'); return; } if (errorHours > 0 && !errorTypeId) { showMessage('부적합 처리 시간이 있는 경우 원인을 선택해주세요.', 'error'); return; } const reportData = { worker_id: parseInt(workerId), project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), task_id: parseInt(taskId), report_date: reportDate, workplace_category_id: parseInt(workplaceCategoryId), workplace_id: parseInt(workplaceId), start_time: null, end_time: null, total_hours: totalHours, error_hours: errorHours, error_type_id: errorTypeId ? parseInt(errorTypeId) : null, work_status_id: errorHours > 0 ? 2 : 1 }; try { const response = await window.apiCall('/daily-work-reports', 'POST', reportData); if (!response.success) { throw new Error(response.message || '작업보고서 제출 실패'); } showSaveResultModal( 'success', '작업보고서 제출 완료', '작업보고서가 성공적으로 제출되었습니다.' ); // 행 제거 removeManualWorkRow(manualIndex); // 목록 새로고침 await loadIncompleteTbms(); } catch (error) { console.error('수동 작업보고서 제출 오류:', error); showSaveResultModal('error', '제출 실패', error.message); } }; /** * 날짜 포맷 함수 */ function formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * 작성 완료된 작업보고서 로드 */ window.loadCompletedReports = async function() { try { const selectedDate = document.getElementById('completedReportDate').value; if (!selectedDate) { showMessage('날짜를 선택해주세요.', 'error'); return; } // 해당 날짜의 작업보고서 조회 const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}`); console.log('완료된 보고서 API 응답:', response); // API 응답이 배열인지 객체인지 확인 let reports = []; if (Array.isArray(response)) { reports = response; } else if (response.success && response.data) { reports = Array.isArray(response.data) ? response.data : []; } else if (response.data) { reports = Array.isArray(response.data) ? response.data : []; } renderCompletedReports(reports); } catch (error) { console.error('완료된 보고서 로드 오류:', error); showMessage('작업보고서를 불러오는 중 오류가 발생했습니다.', 'error'); } }; /** * 완료된 보고서 목록 렌더링 */ function renderCompletedReports(reports) { const container = document.getElementById('completedReportsList'); if (!reports || reports.length === 0) { container.innerHTML = '

작성된 작업보고서가 없습니다.

'; return; } const html = reports.map(report => `

${report.worker_name || '작업자'}

${report.tbm_session_id ? 'TBM 연동' : '수동 입력'}
${formatDate(report.report_date)}
프로젝트: ${report.project_name || '-'}
공정: ${report.work_type_name || '-'}
작업시간: ${report.total_hours || report.work_hours || 0}시간
${report.regular_hours !== undefined && report.regular_hours !== null ? `
정규 시간: ${report.regular_hours}시간
` : ''} ${report.error_hours && report.error_hours > 0 ? `
부적합 처리: ${report.error_hours}시간
부적합 원인: ${report.error_type_name || '-'}
` : ''}
작성자: ${report.created_by_name || '-'}
${report.start_time && report.end_time ? `
작업 시간: ${report.start_time} ~ ${report.end_time}
` : ''}
`).join(''); container.innerHTML = html; } // ================================================================= // 기존 함수들 // ================================================================= // 한국 시간 기준 오늘 날짜 가져오기 function getKoreaToday() { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // 현재 로그인한 사용자 정보 가져오기 function getCurrentUser() { try { const token = localStorage.getItem('token'); if (!token) return null; const payloadBase64 = token.split('.')[1]; if (payloadBase64) { const payload = JSON.parse(atob(payloadBase64)); console.log('토큰에서 추출한 사용자 정보:', payload); return payload; } } catch (error) { console.log('토큰에서 사용자 정보 추출 실패:', error); } try { const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser'); if (userInfo) { const parsed = JSON.parse(userInfo); console.log('localStorage에서 가져온 사용자 정보:', parsed); return parsed; } } catch (error) { console.log('localStorage에서 사용자 정보 가져오기 실패:', error); } return null; } // 메시지 표시 function showMessage(message, type = 'info') { const container = document.getElementById('message-container'); container.innerHTML = `
${message}
`; if (type === 'success') { setTimeout(() => { hideMessage(); }, 5000); } } function hideMessage() { document.getElementById('message-container').innerHTML = ''; } // 저장 결과 모달 표시 function showSaveResultModal(type, title, message, details = null) { const modal = document.getElementById('saveResultModal'); const titleElement = document.getElementById('resultModalTitle'); const contentElement = document.getElementById('resultModalContent'); // 아이콘 설정 let icon = ''; switch (type) { case 'success': icon = '✅'; break; case 'error': icon = '❌'; break; case 'warning': icon = '⚠️'; break; default: icon = 'ℹ️'; } // 모달 내용 구성 let content = `
${icon}

${title}

${message}

`; // 상세 정보가 있으면 추가 if (details) { if (Array.isArray(details) && details.length > 0) { content += `

상세 정보:

`; } else if (typeof details === 'string') { content += `

${details}

`; } } titleElement.textContent = '저장 결과'; contentElement.innerHTML = content; modal.style.display = 'flex'; // ESC 키로 닫기 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { closeSaveResultModal(); } }); // 배경 클릭으로 닫기 modal.addEventListener('click', function (e) { if (e.target === modal) { closeSaveResultModal(); } }); } // 저장 결과 모달 닫기 function closeSaveResultModal() { const modal = document.getElementById('saveResultModal'); modal.style.display = 'none'; // 이벤트 리스너 제거 document.removeEventListener('keydown', closeSaveResultModal); } // 전역에서 접근 가능하도록 window에 할당 window.closeSaveResultModal = closeSaveResultModal; // 단계 이동 function goToStep(stepNumber) { for (let i = 1; i <= 3; i++) { const step = document.getElementById(`step${i}`); if (step) { step.classList.remove('active', 'completed'); if (i < stepNumber) { step.classList.add('completed'); const stepNum = step.querySelector('.step-number'); if (stepNum) stepNum.classList.add('completed'); } else if (i === stepNumber) { step.classList.add('active'); } } } // 진행 단계 표시 업데이트 updateProgressSteps(stepNumber); currentStep = stepNumber; } // 진행 단계 표시 업데이트 function updateProgressSteps(currentStepNumber) { for (let i = 1; i <= 3; i++) { const progressStep = document.getElementById(`progressStep${i}`); if (progressStep) { progressStep.classList.remove('active', 'completed'); if (i < currentStepNumber) { progressStep.classList.add('completed'); } else if (i === currentStepNumber) { progressStep.classList.add('active'); } } } } // 초기 데이터 로드 (통합 API 사용) async function loadData() { try { showMessage('데이터를 불러오는 중...', 'loading'); console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩 시작...'); await loadWorkers(); await loadProjects(); await loadWorkTypes(); await loadWorkStatusTypes(); await loadErrorTypes(); console.log('로드된 작업자 수:', workers.length); console.log('로드된 프로젝트 수:', projects.length); console.log('작업 유형 수:', workTypes.length); hideMessage(); } catch (error) { console.error('데이터 로드 실패:', error); showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error'); } } async function loadWorkers() { try { console.log('Workers API 호출 중... (통합 API 사용)'); // 모든 작업자 1000명까지 조회 const data = await window.apiCall(`${window.API}/workers?limit=1000`); const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []); // 작업 보고서에 표시할 작업자만 필터링 // 퇴사자만 제외 (계정 여부와 무관하게 재직자는 모두 표시) workers = allWorkers.filter(worker => { const notResigned = worker.employment_status !== 'resigned'; return notResigned; }); console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`); console.log(`📊 필터링 조건: employment_status≠resigned (퇴사자만 제외)`); } catch (error) { console.error('작업자 로딩 오류:', error); throw error; } } async function loadProjects() { try { console.log('Projects API 호출 중... (활성 프로젝트만)'); const data = await window.apiCall(`${window.API}/projects/active/list`); projects = Array.isArray(data) ? data : (data.data || data.projects || []); console.log('✅ 활성 프로젝트 로드 성공:', projects.length); } catch (error) { console.error('프로젝트 로딩 오류:', error); throw error; } } async function loadWorkTypes() { try { const data = await window.apiCall(`${window.API}/daily-work-reports/work-types`); if (Array.isArray(data) && data.length > 0) { workTypes = data; console.log('✅ 작업 유형 API 사용 (통합 설정)'); return; } throw new Error('API 실패'); } catch (error) { console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용'); workTypes = [ { id: 1, name: 'Base' }, { id: 2, name: 'Vessel' }, { id: 3, name: 'Piping' } ]; } } async function loadWorkStatusTypes() { try { const data = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`); if (Array.isArray(data) && data.length > 0) { workStatusTypes = data; console.log('✅ 업무 상태 유형 API 사용 (통합 설정)'); return; } throw new Error('API 실패'); } catch (error) { console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용'); workStatusTypes = [ { id: 1, name: '정규' }, { id: 2, name: '에러' } ]; } } async function loadErrorTypes() { try { const data = await window.apiCall(`${window.API}/daily-work-reports/error-types`); if (Array.isArray(data) && data.length > 0) { errorTypes = data; console.log('✅ 에러 유형 API 사용 (통합 설정)'); return; } throw new Error('API 실패'); } catch (error) { console.log('⚠️ 에러 유형 API 사용 불가, 기본값 사용'); errorTypes = [ { id: 1, name: '설계미스' }, { id: 2, name: '외주작업 불량' }, { id: 3, name: '입고지연' }, { id: 4, name: '작업 불량' } ]; } } // TBM 팀 구성 자동 불러오기 async function loadTbmTeamForDate(date) { try { console.log('🛠️ TBM 팀 구성 조회 중:', date); const response = await window.apiCall(`/tbm/sessions/date/${date}`); if (response && response.success && response.data && response.data.length > 0) { // 가장 최근 세션 선택 (진행중인 세션 우선) const draftSessions = response.data.filter(s => s.status === 'draft'); const targetSession = draftSessions.length > 0 ? draftSessions[0] : response.data[0]; if (targetSession) { // 팀 구성 조회 const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`); if (teamRes && teamRes.success && teamRes.data) { const teamWorkerIds = teamRes.data.map(m => m.worker_id); console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}명`); return teamWorkerIds; } } } console.log('ℹ️ 해당 날짜의 TBM 팀 구성이 없습니다.'); return []; } catch (error) { console.error('❌ TBM 팀 구성 조회 오류:', error); return []; } } // 작업자 그리드 생성 async function populateWorkerGrid() { const grid = document.getElementById('workerGrid'); grid.innerHTML = ''; // 선택된 날짜의 TBM 팀 구성 불러오기 const reportDate = document.getElementById('reportDate').value; let tbmWorkerIds = []; if (reportDate) { tbmWorkerIds = await loadTbmTeamForDate(reportDate); } // TBM 팀 구성이 있으면 안내 메시지 표시 if (tbmWorkerIds.length > 0) { const infoDiv = document.createElement('div'); infoDiv.style.cssText = ` padding: 1rem; background: #eff6ff; border: 1px solid #3b82f6; border-radius: 0.5rem; margin-bottom: 1rem; color: #1e40af; font-size: 0.875rem; `; infoDiv.innerHTML = ` 🛠️ TBM 팀 구성 자동 적용
오늘 TBM에서 구성된 팀원 ${tbmWorkerIds.length}명이 자동으로 선택되었습니다. `; grid.appendChild(infoDiv); } workers.forEach(worker => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'worker-card'; btn.textContent = worker.worker_name; btn.dataset.id = worker.worker_id; // TBM 팀 구성에 포함된 작업자는 자동 선택 if (tbmWorkerIds.includes(worker.worker_id)) { btn.classList.add('selected'); selectedWorkers.add(worker.worker_id); } btn.addEventListener('click', () => { toggleWorkerSelection(worker.worker_id, btn); }); grid.appendChild(btn); }); // 자동 선택된 작업자가 있으면 다음 단계 버튼 활성화 const nextBtn = document.getElementById('nextStep2'); if (nextBtn) { nextBtn.disabled = selectedWorkers.size === 0; } } // 작업자 선택 토글 function toggleWorkerSelection(workerId, btnElement) { if (selectedWorkers.has(workerId)) { selectedWorkers.delete(workerId); btnElement.classList.remove('selected'); } else { selectedWorkers.add(workerId); btnElement.classList.add('selected'); } const nextBtn = document.getElementById('nextStep2'); nextBtn.disabled = selectedWorkers.size === 0; } // 작업 항목 추가 function addWorkEntry() { console.log('🔧 addWorkEntry 함수 호출됨'); const container = document.getElementById('workEntriesList'); console.log('🔧 컨테이너:', container); workEntryCounter++; console.log('🔧 작업 항목 카운터:', workEntryCounter); const entryDiv = document.createElement('div'); entryDiv.className = 'work-entry'; entryDiv.dataset.id = workEntryCounter; console.log('🔧 생성된 작업 항목 div:', entryDiv); entryDiv.innerHTML = `
작업 항목 #${workEntryCounter}
🏗️ 프로젝트
⚙️ 작업 유형
📊 업무 상태
⚠️ 에러 유형
작업 시간 (시간)
`; container.appendChild(entryDiv); console.log('🔧 작업 항목이 컨테이너에 추가됨'); console.log('🔧 현재 컨테이너 내용:', container.innerHTML.length, '문자'); console.log('🔧 현재 .work-entry 개수:', container.querySelectorAll('.work-entry').length); setupWorkEntryEvents(entryDiv); console.log('🔧 이벤트 설정 완료'); } // 작업 항목 이벤트 설정 function setupWorkEntryEvents(entryDiv) { const timeInput = entryDiv.querySelector('.time-input'); const workStatusSelect = entryDiv.querySelector('.work-status-select'); const errorTypeSection = entryDiv.querySelector('.error-type-section'); const errorTypeSelect = entryDiv.querySelector('.error-type-select'); // 시간 입력 이벤트 timeInput.addEventListener('input', updateTotalHours); // 빠른 시간 버튼 이벤트 entryDiv.querySelectorAll('.quick-time-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); timeInput.value = btn.dataset.hours; updateTotalHours(); // 버튼 클릭 효과 btn.style.transform = 'scale(0.95)'; setTimeout(() => { btn.style.transform = ''; }, 150); }); }); // 업무 상태 변경 시 에러 유형 섹션 토글 workStatusSelect.addEventListener('change', (e) => { const isError = e.target.value === '2'; // 에러 상태 ID가 2라고 가정 if (isError) { errorTypeSection.classList.add('visible'); errorTypeSelect.required = true; // 에러 상태일 때 시각적 피드백 errorTypeSection.style.animation = 'slideDown 0.4s ease-out'; } else { errorTypeSection.classList.remove('visible'); errorTypeSelect.required = false; errorTypeSelect.value = ''; } }); // 폼 필드 포커스 효과 entryDiv.querySelectorAll('.form-field-group').forEach(group => { const input = group.querySelector('select, input'); if (input) { input.addEventListener('focus', () => { group.classList.add('focused'); }); input.addEventListener('blur', () => { group.classList.remove('focused'); }); } }); } // 작업 항목 제거 function removeWorkEntry(id) { console.log('🗑️ removeWorkEntry 호출됨, id:', id); const entry = document.querySelector(`.work-entry[data-id="${id}"]`); console.log('🗑️ 찾은 entry:', entry); if (entry) { entry.remove(); updateTotalHours(); console.log('✅ 작업 항목 삭제 완료'); } else { console.log('❌ 작업 항목을 찾을 수 없음'); } } // 총 시간 업데이트 function updateTotalHours() { const timeInputs = document.querySelectorAll('.time-input'); let total = 0; timeInputs.forEach(input => { const value = parseFloat(input.value) || 0; total += value; }); const display = document.getElementById('totalHoursDisplay'); display.textContent = `총 작업시간: ${total}시간`; if (total > 24) { display.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)'; display.textContent += ' ⚠️ 24시간 초과'; } else { display.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; } } // 저장 함수 (통합 API 사용) async function saveWorkReport() { const reportDate = document.getElementById('reportDate').value; if (!reportDate || selectedWorkers.size === 0) { showSaveResultModal( 'error', '입력 오류', '날짜와 작업자를 선택해주세요.' ); return; } const entries = document.querySelectorAll('.work-entry'); console.log('🔍 찾은 작업 항목들:', entries); console.log('🔍 작업 항목 개수:', entries.length); if (entries.length === 0) { showSaveResultModal( 'error', '작업 항목 없음', '최소 하나의 작업을 추가해주세요.' ); return; } const newWorkEntries = []; console.log('🔍 작업 항목 수집 시작...'); for (const entry of entries) { console.log('🔍 작업 항목 처리 중:', entry); const projectSelect = entry.querySelector('.project-select'); const workTypeSelect = entry.querySelector('.work-type-select'); const workStatusSelect = entry.querySelector('.work-status-select'); const errorTypeSelect = entry.querySelector('.error-type-select'); const timeInput = entry.querySelector('.time-input'); console.log('🔍 선택된 요소들:', { projectSelect, workTypeSelect, workStatusSelect, errorTypeSelect, timeInput }); const projectId = projectSelect?.value; const workTypeId = workTypeSelect?.value; const workStatusId = workStatusSelect?.value; const errorTypeId = errorTypeSelect?.value; const workHours = timeInput?.value; console.log('🔍 수집된 값들:', { projectId, workTypeId, workStatusId, errorTypeId, workHours }); if (!projectId || !workTypeId || !workStatusId || !workHours) { showSaveResultModal( 'error', '입력 오류', '모든 작업 항목을 완성해주세요.' ); return; } if (workStatusId === '2' && !errorTypeId) { showSaveResultModal( 'error', '입력 오류', '에러 상태인 경우 에러 유형을 선택해주세요.' ); return; } const workEntry = { project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), work_status_id: parseInt(workStatusId), error_type_id: errorTypeId ? parseInt(errorTypeId) : null, work_hours: parseFloat(workHours) }; console.log('🔍 생성된 작업 항목:', workEntry); console.log('🔍 작업 항목 상세:', { project_id: workEntry.project_id, work_type_id: workEntry.work_type_id, work_status_id: workEntry.work_status_id, error_type_id: workEntry.error_type_id, work_hours: workEntry.work_hours }); newWorkEntries.push(workEntry); } console.log('🔍 최종 수집된 작업 항목들:', newWorkEntries); console.log('🔍 총 작업 항목 개수:', newWorkEntries.length); try { const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = true; submitBtn.textContent = '💾 저장 중...'; const currentUser = getCurrentUser(); let totalSaved = 0; let totalFailed = 0; const failureDetails = []; for (const workerId of selectedWorkers) { const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음'; // 서버가 기대하는 work_entries 배열 형태로 전송 const requestData = { report_date: reportDate, worker_id: parseInt(workerId), work_entries: newWorkEntries.map(entry => ({ project_id: entry.project_id, task_id: entry.work_type_id, // 서버에서 task_id로 기대 work_hours: entry.work_hours, work_status_id: entry.work_status_id, error_type_id: entry.error_type_id })), created_by: currentUser?.user_id || currentUser?.id }; console.log('🔄 배열 형태로 전송:', requestData); console.log('🔄 work_entries:', requestData.work_entries); console.log('🔄 work_entries[0] 상세:', requestData.work_entries[0]); console.log('🔄 전송 데이터 JSON:', JSON.stringify(requestData, null, 2)); try { const result = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', requestData); console.log('✅ 저장 성공:', result); totalSaved++; } catch (error) { console.error('❌ 저장 실패:', error); totalFailed++; failureDetails.push(`${workerName}: ${error.message}`); } } // 결과 모달 표시 if (totalSaved > 0 && totalFailed === 0) { showSaveResultModal( 'success', '저장 완료!', `${totalSaved}명의 작업보고서가 성공적으로 저장되었습니다.` ); } else if (totalSaved > 0 && totalFailed > 0) { showSaveResultModal( 'warning', '부분 저장 완료', `${totalSaved}명은 성공했지만 ${totalFailed}명은 실패했습니다.`, failureDetails ); } else { showSaveResultModal( 'error', '저장 실패', '모든 작업보고서 저장이 실패했습니다.', failureDetails ); } if (totalSaved > 0) { setTimeout(() => { refreshTodayWorkers(); resetForm(); }, 2000); } } catch (error) { console.error('저장 오류:', error); showSaveResultModal( 'error', '저장 오류', '저장 중 예기치 못한 오류가 발생했습니다.', [error.message] ); } finally { const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = false; submitBtn.textContent = '💾 작업보고서 저장'; } } // 폼 초기화 function resetForm() { goToStep(1); selectedWorkers.clear(); document.querySelectorAll('.worker-card.selected').forEach(btn => { btn.classList.remove('selected'); }); const container = document.getElementById('workEntriesList'); container.innerHTML = ''; workEntryCounter = 0; updateTotalHours(); document.getElementById('nextStep2').disabled = true; } // 당일 작업자 현황 로드 (본인 입력분만) - 통합 API 사용 async function loadTodayWorkers() { const section = document.getElementById('dailyWorkersSection'); const content = document.getElementById('dailyWorkersContent'); if (!section || !content) { console.log('당일 현황 섹션이 HTML에 없습니다.'); return; } try { const today = getKoreaToday(); const currentUser = getCurrentUser(); content.innerHTML = '
📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)
'; section.style.display = 'block'; // 본인이 입력한 데이터만 조회 (통합 API 사용) let queryParams = `date=${today}`; if (currentUser?.user_id) { queryParams += `&created_by=${currentUser.user_id}`; } else if (currentUser?.id) { queryParams += `&created_by=${currentUser.id}`; } console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`); const rawData = await window.apiCall(`${window.API}/daily-work-reports?${queryParams}`); console.log('📊 당일 작업 데이터 (통합 API):', rawData); let data = []; if (Array.isArray(rawData)) { data = rawData; } else if (rawData?.data) { data = rawData.data; } displayMyDailyWorkers(data, today); } catch (error) { console.error('당일 작업자 로드 오류:', error); content.innerHTML = `
❌ 오늘의 작업 현황을 불러올 수 없습니다.
${error.message}
`; } } // 본인 입력 작업자 현황 표시 (수정/삭제 기능 포함) function displayMyDailyWorkers(data, date) { const content = document.getElementById('dailyWorkersContent'); if (!Array.isArray(data) || data.length === 0) { content.innerHTML = `
📝 내가 오늘(${date}) 입력한 작업이 없습니다.
새로운 작업을 추가해보세요!
`; return; } // 작업자별로 데이터 그룹화 const workerGroups = {}; data.forEach(work => { const workerName = work.worker_name || '미지정'; if (!workerGroups[workerName]) { workerGroups[workerName] = []; } workerGroups[workerName].push(work); }); const totalWorkers = Object.keys(workerGroups).length; const totalWorks = data.length; const headerHtml = `

📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업

`; const workersHtml = Object.entries(workerGroups).map(([workerName, works]) => { const totalHours = works.reduce((sum, work) => { return sum + parseFloat(work.work_hours || 0); }, 0); // 개별 작업 항목들 (수정/삭제 버튼 포함) const individualWorksHtml = works.map((work) => { const projectName = work.project_name || '미지정'; const workTypeName = work.work_type_name || '미지정'; const workStatusName = work.work_status_name || '미지정'; const workHours = work.work_hours || 0; const errorTypeName = work.error_type_name || null; const workId = work.id; return `
🏗️ 프로젝트
${projectName}
⚙️ 작업종류
${workTypeName}
📊 작업상태
${workStatusName}
⏰ 작업시간
${workHours}시간
${errorTypeName ? `
❌ 에러유형
${errorTypeName}
` : ''}
`; }).join(''); return `
👤 ${workerName}
총 ${totalHours}시간
${individualWorksHtml}
`; }).join(''); content.innerHTML = headerHtml + '
' + workersHtml + '
'; } // 작업 항목 수정 함수 (통합 API 사용) async function editWorkItem(workId) { try { console.log('수정할 작업 ID:', workId); // 1. 기존 데이터 조회 (통합 API 사용) showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading'); const workData = await window.apiCall(`${window.API}/daily-work-reports/${workId}`); console.log('수정할 작업 데이터 (통합 API):', workData); // 2. 수정 모달 표시 showEditModal(workData); hideMessage(); } catch (error) { console.error('작업 정보 조회 오류:', error); showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error'); } } // 수정 모달 표시 function showEditModal(workData) { editingWorkId = workData.id; const modalHtml = `

✏️ 작업 수정

`; document.body.insertAdjacentHTML('beforeend', modalHtml); // 업무 상태 변경 이벤트 document.getElementById('editWorkStatus').addEventListener('change', (e) => { const errorTypeGroup = document.getElementById('editErrorTypeGroup'); if (e.target.value === '2') { errorTypeGroup.style.display = 'block'; } else { errorTypeGroup.style.display = 'none'; } }); } // 수정 모달 닫기 function closeEditModal() { const modal = document.getElementById('editModal'); if (modal) { modal.remove(); } editingWorkId = null; } // 수정된 작업 저장 (통합 API 사용) async function saveEditedWork() { try { const projectId = document.getElementById('editProject').value; const workTypeId = document.getElementById('editWorkType').value; const workStatusId = document.getElementById('editWorkStatus').value; const errorTypeId = document.getElementById('editErrorType').value; const workHours = document.getElementById('editWorkHours').value; if (!projectId || !workTypeId || !workStatusId || !workHours) { showMessage('모든 필수 항목을 입력해주세요.', 'error'); return; } if (workStatusId === '2' && !errorTypeId) { showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error'); return; } const updateData = { project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), work_status_id: parseInt(workStatusId), error_type_id: errorTypeId ? parseInt(errorTypeId) : null, work_hours: parseFloat(workHours) }; showMessage('작업을 수정하는 중... (통합 API)', 'loading'); const result = await window.apiCall(`${window.API}/daily-work-reports/${editingWorkId}`, { method: 'PUT', body: JSON.stringify(updateData) }); console.log('✅ 수정 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success'); closeEditModal(); refreshTodayWorkers(); } catch (error) { console.error('❌ 수정 실패:', error); showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error'); } } // 작업 항목 삭제 함수 (통합 API 사용) async function deleteWorkItem(workId) { if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) { return; } try { console.log('삭제할 작업 ID:', workId); showMessage('작업을 삭제하는 중... (통합 API)', 'loading'); // 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용 const result = await window.apiCall(`${window.API}/daily-work-reports/my-entry/${workId}`, { method: 'DELETE' }); console.log('✅ 삭제 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success'); // 화면 새로고침 refreshTodayWorkers(); } catch (error) { console.error('❌ 삭제 실패:', error); showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error'); } } // 오늘 현황 새로고침 function refreshTodayWorkers() { loadTodayWorkers(); } // 이벤트 리스너 설정 (이제 테이블 기반 UI를 사용하므로 별도 리스너 불필요) function setupEventListeners() { // 기존 단계별 입력 UI 제거됨 // 모든 이벤트는 onclick 핸들러로 직접 처리 } // 초기화 async function init() { try { const token = localStorage.getItem('token'); if (!token || token === 'undefined') { showMessage('로그인이 필요합니다.', 'error'); localStorage.removeItem('token'); setTimeout(() => { window.location.href = '/'; }, 2000); return; } await loadData(); setupEventListeners(); // TBM 작업 목록 로드 (기본 탭) await loadIncompleteTbms(); console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)'); } catch (error) { console.error('초기화 오류:', error); showMessage('초기화 중 오류가 발생했습니다.', 'error'); } } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', init); // 전역 함수로 노출 window.removeWorkEntry = removeWorkEntry; window.refreshTodayWorkers = refreshTodayWorkers; window.editWorkItem = editWorkItem; window.deleteWorkItem = deleteWorkItem; window.closeEditModal = closeEditModal; window.saveEditedWork = saveEditedWork; // ================================================================= // 시간 선택 팝오버 관련 함수 // ================================================================= /** * 시간 포맷팅 함수 */ function formatHours(hours) { const h = Math.floor(hours); const m = (hours % 1) * 60; if (m === 0) return `${h}시간`; return `${h}시간 ${m}분`; } /** * 시간 선택 팝오버 열기 */ window.openTimePicker = function(index, type) { currentEditingField = { index, type }; // 현재 값 가져오기 const inputId = type === 'total' ? `totalHours_${index}` : `errorHours_${index}`; const hiddenInput = document.getElementById(inputId); currentTimeValue = parseFloat(hiddenInput?.value) || 0; // 팝오버 표시 const overlay = document.getElementById('timePickerOverlay'); const title = document.getElementById('timePickerTitle'); title.textContent = type === 'total' ? '작업시간 선택' : '부적합 시간 선택'; updateTimeDisplay(); overlay.style.display = 'flex'; // ESC 키로 닫기 document.addEventListener('keydown', handleEscapeKey); }; /** * ESC 키 핸들러 */ function handleEscapeKey(e) { if (e.key === 'Escape') { closeTimePicker(); } } /** * 시간 값 설정 */ window.setTimeValue = function(hours) { currentTimeValue = hours; updateTimeDisplay(); }; /** * 시간 조정 (±30분) */ window.adjustTime = function(delta) { currentTimeValue = Math.max(0, Math.min(24, currentTimeValue + delta)); updateTimeDisplay(); }; /** * 시간 표시 업데이트 */ function updateTimeDisplay() { const display = document.getElementById('currentTimeDisplay'); if (display) { display.textContent = formatHours(currentTimeValue); } } /** * 시간 선택 확인 */ window.confirmTimeSelection = function() { if (!currentEditingField) return; const { index, type } = currentEditingField; const inputId = type === 'total' ? `totalHours_${index}` : `errorHours_${index}`; const displayId = type === 'total' ? `totalHoursDisplay_${index}` : `errorHoursDisplay_${index}`; // hidden input 값 설정 const hiddenInput = document.getElementById(inputId); if (hiddenInput) { hiddenInput.value = currentTimeValue; } // 표시 영역 업데이트 const displayDiv = document.getElementById(displayId); if (displayDiv) { displayDiv.textContent = formatHours(currentTimeValue); displayDiv.classList.remove('placeholder'); displayDiv.classList.add('has-value'); } // 부적합 시간 입력 시 에러 타입 토글 if (type === 'error') { if (index.toString().startsWith('manual_')) { toggleManualErrorType(index); } else { calculateRegularHours(index); } } closeTimePicker(); }; /** * 시간 선택 팝오버 닫기 */ window.closeTimePicker = function() { const overlay = document.getElementById('timePickerOverlay'); if (overlay) { overlay.style.display = 'none'; } currentEditingField = null; currentTimeValue = 0; // ESC 키 리스너 제거 document.removeEventListener('keydown', handleEscapeKey); };