// daily-work-report.js - 브라우저 호환 버전 // ================================================================= // 🌐 API 설정 (window 객체에서 가져오기) // ================================================================= // API 설정은 api-config.js에서 window 객체에 설정됨 // 전역 변수 → DailyWorkReportState 프록시 사용 (state.js에서 window 프록시 정의) // workTypes, workStatusTypes, errorTypes, issueCategories, issueItems, // workers, projects, selectedWorkers, incompleteTbms, tempDefects, // dailyIssuesCache, currentTab, currentStep, editingWorkId, workEntryCounter, // currentDefectIndex, currentEditingField, currentTimeValue, // selectedWorkplace, selectedWorkplaceName, selectedWorkplaceCategory, selectedWorkplaceCategoryName // 지도 관련 변수 (프록시 아님) var mapCanvas = null; var mapCtx = null; var mapImage = null; var mapRegions = []; // 시간 선택 관련 변수 // currentEditingField, currentTimeValue → DailyWorkReportState 프록시 사용 // 당일 신고 리마인더 관련 변수 // dailyIssuesCache → DailyWorkReportState 프록시 사용 // ================================================================= // 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; // 각 세션 날짜에 대해 관련 신고 조회 await loadDailyIssuesForTbms(); renderTbmWorkList(); } catch (error) { console.error('미완료 TBM 로드 오류:', error); showMessage('TBM 작업 목록을 불러오는 중 오류가 발생했습니다.', 'error'); } } /** * TBM 세션들에 대한 당일 신고 조회 * - 각 세션 날짜별로 관련 신고를 가져와서 캐시에 저장 */ async function loadDailyIssuesForTbms() { if (!incompleteTbms || incompleteTbms.length === 0) { console.log('[작업보고서] 미완료 TBM 없음, 신고 조회 건너뜀'); return; } console.log('[작업보고서] TBM 세션 수:', incompleteTbms.length); // 세션별로 고유한 날짜 + 작업장 조합 수집 const sessionKeys = new Set(); incompleteTbms.forEach(tbm => { const dateStr = formatDateForApi(tbm.session_date); console.log('[작업보고서] TBM 세션 날짜:', tbm.session_date, '→ 변환:', dateStr); if (dateStr) { // 날짜_작업장ID_프로젝트ID 형태로 키 생성 const key = `${dateStr}_${tbm.workplace_id || 0}_${tbm.project_id || 0}`; sessionKeys.add(key); } }); // 각 날짜에 대해 신고 조회 const uniqueDates = [...new Set([...sessionKeys].map(k => k.split('_')[0]))]; console.log('[작업보고서] 조회할 날짜들:', uniqueDates); for (const dateStr of uniqueDates) { if (dailyIssuesCache[dateStr]) { console.log(`[작업보고서] 캐시 사용 (${dateStr}):`, dailyIssuesCache[dateStr].length, '건'); continue; } try { console.log(`[작업보고서] 신고 API 호출: /work-issues?start_date=${dateStr}&end_date=${dateStr}`); const response = await window.apiCall(`/work-issues?start_date=${dateStr}&end_date=${dateStr}`); if (response.success) { dailyIssuesCache[dateStr] = response.data || []; console.log(`[작업보고서] 신고 로드 완료 (${dateStr}):`, dailyIssuesCache[dateStr].length, '건'); } else { console.warn(`[작업보고서] 신고 API 실패:`, response); dailyIssuesCache[dateStr] = []; } } catch (error) { console.error(`[작업보고서] 신고 조회 오류 (${dateStr}):`, error); dailyIssuesCache[dateStr] = []; } } console.log('[작업보고서] 전체 신고 캐시:', dailyIssuesCache); } /** * 특정 날짜의 모든 신고 반환 (작업장소 관계없이) * - 참고용으로 해당 날짜에 발생한 모든 신고를 표시 * @param {string} dateStr - 날짜 (YYYY-MM-DD) * @param {number} workplaceId - 작업장소 ID (현재 미사용, 향후 하이라이트 용도) * @param {number} projectId - 프로젝트 ID (현재 미사용) * @returns {Array} 해당 날짜의 모든 신고 목록 */ function getRelatedIssues(dateStr, workplaceId, projectId) { const issues = dailyIssuesCache[dateStr] || []; // 해당 날짜의 모든 신고를 반환 (작업장소 필터 제거) // 사용자가 참고하여 관련 여부를 직접 판단하도록 함 return issues; } /** * 날짜를 API 형식(YYYY-MM-DD)으로 변환 */ function formatDateForApi(date) { if (window.CommonUtils) return window.CommonUtils.formatDate(date) || null; if (!date) return null; const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return null; return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); } /** * 사용자 정보 가져오기 (auth-check.js와 동일한 로직) */ function getUser() { if (window.getSSOUser) return window.getSSOUser(); const raw = localStorage.getItem('sso_user') || localStorage.getItem('user'); try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; } } /** * TBM 작업 목록 렌더링 (날짜별 > 세션별 그룹화) * - 날짜별로 접기/펼치기 가능 * - 날짜 헤더에 이슈 요약 표시 */ function renderTbmWorkList() { const container = document.getElementById('tbmWorkList'); // 1단계: 날짜별로 그룹화 const byDate = {}; if (incompleteTbms && incompleteTbms.length > 0) { incompleteTbms.forEach((tbm, index) => { const dateStr = formatDateForApi(tbm.session_date); if (!byDate[dateStr]) { byDate[dateStr] = { date: tbm.session_date, sessions: {} }; } // 2단계: 날짜 내에서 세션별로 그룹화 const sessionKey = `${tbm.session_id}_${dateStr}`; if (!byDate[dateStr].sessions[sessionKey]) { byDate[dateStr].sessions[sessionKey] = { session_id: tbm.session_id, session_date: tbm.session_date, created_by_name: tbm.leader_name || tbm.created_by_name || '-', items: [] }; } byDate[dateStr].sessions[sessionKey].items.push({ ...tbm, originalIndex: index }); }); } // 날짜 정렬 (최신순) const sortedDates = Object.keys(byDate).sort((a, b) => new Date(b) - new Date(a)); // 레거시 호환: 기존 groupedTbms 구조도 유지 const groupedTbms = {}; sortedDates.forEach(dateStr => { Object.assign(groupedTbms, byDate[dateStr].sessions); }); let html = `

작업보고서 목록

`; // 수동 입력 섹션 먼저 추가 (맨 위) html += `
수동 입력 TBM에 없는 작업을 추가로 입력할 수 있습니다
작업자 날짜 프로젝트 공정 작업 작업장소 작업시간 부적합 제출
`; // 날짜별로 테이블 생성 (접기/펼치기 가능) sortedDates.forEach((dateStr, dateIndex) => { const dateData = byDate[dateStr]; const sessions = Object.values(dateData.sessions); const totalWorkers = sessions.reduce((sum, s) => sum + s.items.length, 0); // 해당 날짜의 모든 신고 조회 const relatedIssues = getRelatedIssues(dateStr); const nonconformityCount = relatedIssues.filter(i => i.category_type === 'nonconformity').length; const safetyCount = relatedIssues.filter(i => i.category_type === 'safety').length; const hasIssues = relatedIssues.length > 0; // 요일 계산 const dayNames = ['일', '월', '화', '수', '목', '금', '토']; const dayOfWeek = dayNames[new Date(dateStr).getDay()]; // 오늘 날짜인지 확인 const today = formatDateForApi(new Date()); const isToday = dateStr === today; // 날짜 그룹 시작 (기본적으로 오늘만 펼침) const isExpanded = isToday || dateIndex === 0; html += `
${isExpanded ? '▼' : '▶'} ${formatDate(dateData.date)} (${dayOfWeek}) ${isToday ? '오늘' : ''}
세션 ${sessions.length}개 작업자 ${totalWorkers}명
${hasIssues ? `
${nonconformityCount > 0 ? `부적합 ${nonconformityCount}` : ''} ${safetyCount > 0 ? `안전 ${safetyCount}` : ''}
` : '신고 없음'}
`; // 신고 리마인더 HTML 생성 (날짜 그룹 내부) if (hasIssues) { html += `
⚠️ 당일 신고된 문제 ${relatedIssues.length}건
${relatedIssues.slice(0, 5).map(issue => { // 아이템명과 추가설명 조합 let itemText = issue.issue_item_name || ''; if (issue.additional_description) { itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description; } return `
${issue.category_type === 'safety' ? '안전' : '부적합'} ${issue.issue_category_name || ''} ${itemText || '-'} ${issue.workplace_name || issue.custom_location || ''} ${getStatusLabel(issue.status)}
`}).join('')} ${relatedIssues.length > 5 ? `
외 ${relatedIssues.length - 5}건 더 있음
` : ''}
💡 위 문제로 인해 작업이 지연되었다면, 아래에서 부적합 시간을 추가해주세요.
`; } // 해당 날짜의 각 세션별로 테이블 생성 sessions.forEach(group => { const key = `${group.session_id}_${dateStr}`; html += `
TBM 세션 작성자: ${group.created_by_name} ${group.items.length}명
${group.items.map(tbm => { const index = tbm.originalIndex; // 이 작업자의 작업장소와 관련된 이슈가 있는지 확인 (부적합 버튼 강조용) const hasRelatedIssue = relatedIssues.some(issue => { if (issue.category_type !== 'nonconformity') return false; // 작업장소 매칭 if (tbm.workplace_id && issue.workplace_id) { return tbm.workplace_id === issue.workplace_id; } if (tbm.workplace_name && (issue.workplace_name || issue.custom_location)) { const issueLocation = issue.workplace_name || issue.custom_location || ''; return issueLocation.includes(tbm.workplace_name) || tbm.workplace_name.includes(issueLocation); } return false; }); return ` `; }).join('')}
작업자 프로젝트 공정 작업 작업장소 작업시간 부적합 제출
${tbm.worker_name || '작업자'}
${tbm.job_type || '-'}
${tbm.project_name || '-'} ${tbm.work_type_name || '-'} ${tbm.task_name || '-'}
${tbm.category_name || ''}
${tbm.workplace_name || '-'}
시간 선택
`; }); // 날짜 그룹 닫기 html += `
`; }); container.innerHTML = html; } /** * 날짜 그룹 접기/펼치기 토글 */ window.toggleDateGroup = function(dateStr) { const group = document.querySelector(`.date-group[data-date="${dateStr}"]`); if (!group) return; const content = group.querySelector('.date-group-content'); const icon = group.querySelector('.date-toggle-icon'); const isExpanded = group.classList.contains('expanded'); if (isExpanded) { group.classList.remove('expanded'); group.classList.add('collapsed'); content.style.display = 'none'; icon.textContent = '▶'; } else { group.classList.remove('collapsed'); group.classList.add('expanded'); content.style.display = 'block'; icon.textContent = '▼'; } }; /** * 부적합 시간 입력 처리 */ 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) { // busy guard - 중복 제출 방지 const submitBtn = document.querySelector(`tr[data-index="${index}"][data-type="tbm"] .btn-submit-compact`); if (submitBtn && submitBtn.classList.contains('is-loading')) return; const tbm = incompleteTbms[index]; const totalHours = parseFloat(document.getElementById(`totalHours_${index}`).value); const defects = tempDefects[index] || []; // 총 부적합 시간 계산 const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0); // item_id를 error_type_id로 사용 (issue_report_items.item_id) const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null; // 필수 필드 검증 if (!totalHours || totalHours <= 0) { showMessage('작업시간을 입력해주세요.', 'error'); return; } if (errorHours > totalHours) { showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error'); return; } // 로딩 상태 시작 if (submitBtn) { submitBtn.classList.add('is-loading'); submitBtn.disabled = true; submitBtn.textContent = '제출 중'; } // 부적합 원인 유효성 검사 (issue_report_id 또는 category_id 또는 error_type_id 필요) console.log('🔍 부적합 검증 시작:', defects.map(d => ({ defect_hours: d.defect_hours, category_id: d.category_id, item_id: d.item_id, error_type_id: d.error_type_id, issue_report_id: d.issue_report_id, _saved: d._saved }))); const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id); if (invalidDefects.length > 0) { console.error('❌ 유효하지 않은 부적합:', invalidDefects); 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.task_id, // task_id를 work_type_id 컬럼에 저장 (직접 작업보고서와 일관성 유지) report_date: reportDate, start_time: null, end_time: null, total_hours: totalHours, error_hours: errorHours, error_type_id: errorTypeId, work_status_id: errorHours > 0 ? 2 : 1 }; console.log('🔍 TBM 제출 데이터:', JSON.stringify(reportData, null, 2)); console.log('🔍 부적합 원인:', defects); try { const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData); if (!response.success) { throw new Error(response.message || '작업보고서 제출 실패'); } // 부적합 원인이 있으면 저장 (이슈 기반 또는 레거시) if (defects.length > 0 && response.data?.report_id) { const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0); console.log('📋 부적합 원인 필터링:', { 전체: defects.length, 유효: validDefects.length, validDefects: validDefects.map(d => ({ category_id: d.category_id, item_id: d.item_id, defect_hours: d.defect_hours, _saved: d._saved })) }); if (validDefects.length > 0) { // 내부 플래그 제거 (백엔드 전송용) const defectsToSend = validDefects.map(d => ({ issue_report_id: d.issue_report_id || null, category_id: d.category_id || null, item_id: d.item_id || null, error_type_id: d.error_type_id || null, defect_hours: d.defect_hours, note: d.note || '' })); console.log('📤 부적합 저장 요청:', defectsToSend); const defectResponse = await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', { defects: defectsToSend }); if (!defectResponse.success) { console.error('❌ 부적합 저장 실패:', defectResponse); showMessage('작업보고서는 저장되었으나 부적합 원인 저장에 실패했습니다.', 'warning'); } else { console.log('✅ 부적합 저장 성공:', defectResponse); } } else { console.log('⚠️ 유효한 부적합 항목이 없어 저장 건너뜀'); } } showSaveResultModal( 'success', '작업보고서 제출 완료', `${tbm.worker_name}의 작업보고서가 성공적으로 제출되었습니다.`, response.data.tbm_completed ? '모든 팀원의 작업보고서가 제출되어 TBM이 완료되었습니다.' : response.data.completion_status ); // 임시 부적합 데이터 삭제 delete tempDefects[index]; // 목록 새로고침 await loadIncompleteTbms(); } catch (error) { console.error('TBM 작업보고서 제출 오류:', error); showSaveResultModal('error', '제출 실패', error.message); } finally { // 로딩 상태 해제 if (submitBtn) { submitBtn.classList.remove('is-loading'); submitBtn.disabled = false; submitBtn.textContent = '제출'; } } }; /** * TBM 세션 일괄제출 */ window.batchSubmitTbmSession = async function(sessionKey) { // busy guard - 일괄제출 버튼 const batchBtn = document.querySelector(`[data-session-key="${sessionKey}"] ~ .batch-submit-container .btn-batch-submit, .tbm-session-group[data-session-key="${sessionKey}"] .btn-batch-submit`); if (batchBtn && batchBtn.classList.contains('is-loading')) return; // 해당 세션의 모든 항목 가져오기 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.task_id, // task_id를 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 = batchBtn || event.target; submitBtn.classList.add('is-loading'); 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.classList.remove('is-loading'); 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 = `
🗺️ 작업장소
클릭하여 선택
시간 선택
`; tbody.appendChild(newRow); // 부적합 인라인 영역 행 추가 const defectRow = document.createElement('tr'); defectRow.className = 'defect-row'; defectRow.id = `defectRow_${manualIndex}`; defectRow.style.display = 'none'; defectRow.innerHTML = `
`; tbody.appendChild(defectRow); showMessage('새 작업 행이 추가되었습니다. 정보를 입력하고 제출하세요.', 'info'); }; /** * 수동 작업 행 제거 */ window.removeManualWorkRow = function(manualIndex) { const row = document.querySelector(`tr[data-index="${manualIndex}"]`); const defectRow = document.getElementById(`defectRow_${manualIndex}`); if (row) { row.remove(); } if (defectRow) { defectRow.remove(); } // 임시 부적합 데이터도 삭제 delete tempDefects[manualIndex]; }; /** * 공정 선택 시 작업 목록 로드 */ 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 => { const safeId = parseInt(cat.category_id) || 0; const safeName = escapeHtml(cat.category_name); const safeImage = escapeHtml(cat.layout_image || ''); return ` `; }).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 => { const safeId = parseInt(wp.workplace_id) || 0; const safeName = escapeHtml(wp.workplace_name); return ` `; }).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 = `
작업장소 선택됨
🏭 ${escapeHtml(selectedWorkplaceCategoryName)}
📍 ${escapeHtml(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.selectExternalWorkplace = function() { const manualIndex = window.currentManualIndex; // 외부 작업장소 ID는 0 또는 특별한 값으로 설정 (DB에 저장시 처리 필요) const externalCategoryId = 0; const externalCategoryName = '외부'; const externalWorkplaceId = 0; const externalWorkplaceName = '외부 (외근/연차/휴무)'; // hidden input에 값 설정 document.getElementById(`workplaceCategory_${manualIndex}`).value = externalCategoryId; document.getElementById(`workplace_${manualIndex}`).value = externalWorkplaceId; // 선택 결과 표시 const displayDiv = document.getElementById(`workplaceDisplay_${manualIndex}`); if (displayDiv) { displayDiv.innerHTML = `
외부 선택됨
🌐 ${escapeHtml(externalWorkplaceName)}
`; displayDiv.style.background = '#f0f9ff'; displayDiv.style.borderColor = '#0ea5e9'; } // 모달 닫기 document.getElementById('workplaceModal').style.display = 'none'; showMessage('외부 작업장소가 선택되었습니다.', 'success'); }; /** * 수동 작업보고서 제출 */ 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 defects = tempDefects[manualIndex] || []; const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0); // item_id를 error_type_id로 사용 (issue_report_items.item_id) const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null; // 필수 필드 검증 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 === '' || workplaceId === null || workplaceId === undefined) { showMessage('작업장소를 선택해주세요.', 'error'); return; } if (!totalHours || totalHours <= 0) { showMessage('작업시간을 입력해주세요.', 'error'); return; } if (errorHours > totalHours) { showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error'); return; } // 부적합 원인 유효성 검사 (issue_report_id 또는 error_type_id 필요) const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id); if (invalidDefects.length > 0) { showMessage('부적합 시간이 있는 항목은 원인을 선택해주세요.', 'error'); return; } // 서비스 레이어가 기대하는 형식으로 변환 // 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함 const reportData = { report_date: reportDate, worker_id: parseInt(workerId), work_entries: [{ project_id: parseInt(projectId), task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨 work_hours: totalHours, work_status_id: errorHours > 0 ? 2 : 1, error_type_id: errorTypeId ? parseInt(errorTypeId) : null }] }; try { // 429 오류 재시도 로직 포함 let response; let retries = 3; for (let i = 0; i < retries; i++) { try { response = await window.apiCall('/daily-work-reports', 'POST', reportData); break; } catch (err) { if ((err.message?.includes('429') || err.message?.includes('너무 많은 요청')) && i < retries - 1) { const waitTime = (i + 1) * 2000; showMessage(`서버가 바쁩니다. ${waitTime/1000}초 후 재시도...`, 'loading'); await new Promise(r => setTimeout(r, waitTime)); continue; } throw err; } } if (!response.success) { throw new Error(response.message || '작업보고서 제출 실패'); } // 부적합 원인이 있으면 저장 (이슈 기반 또는 레거시) const reportId = response.data?.inserted_ids?.[0] || response.data?.workReport_ids?.[0]; if (defects.length > 0 && reportId) { const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0); if (validDefects.length > 0) { await window.apiCall(`/daily-work-reports/${reportId}/defects`, 'PUT', { defects: validDefects }); } } // 행 제거 (부적합 임시 데이터도 함께 삭제됨) removeManualWorkRow(manualIndex); showMessage('작업보고서가 제출되었습니다.', 'success'); // 남은 행이 없으면 완료 메시지 const remainingRows = document.querySelectorAll('#manualWorkTableBody tr[data-index]'); if (remainingRows.length === 0) { showSaveResultModal( 'success', '작업보고서 제출 완료', '모든 작업보고서가 성공적으로 제출되었습니다.' ); } } catch (error) { console.error('수동 작업보고서 제출 오류:', error); showMessage('제출 실패: ' + error.message, 'error'); } }; /** * 딜레이 함수 */ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** * API 호출 (429 재시도 포함) */ async function apiCallWithRetry(url, method, data, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await window.apiCall(url, method, data); return response; } catch (error) { // 429 Rate Limit 오류인 경우 재시도 if (error.message && error.message.includes('429') || error.message.includes('너무 많은 요청')) { if (attempt < maxRetries) { const waitTime = attempt * 2000; // 2초, 4초, 6초 대기 console.log(`Rate limit 도달. ${waitTime/1000}초 후 재시도... (${attempt}/${maxRetries})`); await delay(waitTime); continue; } } throw error; } } } /** * 수동 작업보고서 일괄 제출 */ window.submitAllManualWorkReports = async function() { const rows = document.querySelectorAll('#manualWorkTableBody tr[data-index]'); if (rows.length === 0) { showMessage('제출할 작업보고서가 없습니다.', 'error'); return; } // 확인 다이얼로그 if (!confirm(`${rows.length}개의 작업보고서를 일괄 제출하시겠습니까?`)) { return; } let successCount = 0; let failCount = 0; const errors = []; let currentIndex = 0; showMessage(`작업보고서 제출 중... (0/${rows.length})`, 'loading'); // 각 행을 순차적으로 제출 (딜레이 포함) for (const row of rows) { currentIndex++; const manualIndex = row.dataset.index; // Rate Limit 방지를 위한 딜레이 (1초) if (currentIndex > 1) { await delay(1000); } showMessage(`작업보고서 제출 중... (${currentIndex}/${rows.length})`, 'loading'); try { 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 defects = tempDefects[manualIndex] || []; const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0); // item_id를 error_type_id로 사용 (issue_report_items.item_id) const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null; // 필수 필드 검증 if (!workerId || !reportDate || !projectId || !workTypeId || !taskId || !totalHours || totalHours <= 0) { errors.push(`행 ${manualIndex}: 필수 항목 누락`); failCount++; continue; } if (workplaceId === '' || workplaceId === null || workplaceId === undefined) { errors.push(`행 ${manualIndex}: 작업장소 미선택`); failCount++; continue; } // 서비스 레이어가 기대하는 형식으로 변환 const reportData = { report_date: reportDate, worker_id: parseInt(workerId), work_entries: [{ project_id: parseInt(projectId), task_id: parseInt(taskId), work_hours: totalHours, work_status_id: errorHours > 0 ? 2 : 1, error_type_id: errorTypeId ? parseInt(errorTypeId) : null }] }; const response = await apiCallWithRetry('/daily-work-reports', 'POST', reportData); if (!response.success) { throw new Error(response.message || '작업보고서 제출 실패'); } // 부적합 원인이 있으면 저장 const reportId = response.data?.inserted_ids?.[0] || response.data?.workReport_ids?.[0]; if (defects.length > 0 && reportId) { const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0); if (validDefects.length > 0) { await apiCallWithRetry(`/daily-work-reports/${reportId}/defects`, 'PUT', { defects: validDefects }); } } // 성공 - 행 제거 removeManualWorkRow(manualIndex); successCount++; } catch (error) { console.error(`행 ${manualIndex} 제출 오류:`, error); errors.push(`행 ${manualIndex}: ${error.message}`); failCount++; } } // 로딩 메시지 숨기기 hideMessage(); // 결과 표시 let resultMessage = `성공: ${successCount}건`; if (failCount > 0) { resultMessage += `, 실패: ${failCount}건`; } if (failCount > 0 && errors.length > 0) { showSaveResultModal( 'warning', '일괄 제출 완료 (일부 실패)', `${resultMessage}\n\n실패 원인:\n${errors.slice(0, 5).join('\n')}${errors.length > 5 ? `\n... 외 ${errors.length - 5}건` : ''}` ); } else { showSaveResultModal( 'success', '일괄 제출 완료', resultMessage ); } }; /** * 날짜 포맷 함수 */ function formatDate(dateString) { if (window.CommonUtils) return window.CommonUtils.formatDate(dateString); return formatDateForApi(dateString); } /** * 신고 상태 라벨 반환 */ function getStatusLabel(status) { const labels = { 'reported': '신고됨', 'received': '접수됨', 'in_progress': '처리중', 'completed': '완료', 'closed': '종료' }; return labels[status] || status || '-'; } /** * 작성 완료된 작업보고서 로드 */ 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 => `

${escapeHtml(report.worker_name || '작업자')}

${report.tbm_session_id ? 'TBM 연동' : '수동 입력'}
${escapeHtml(formatDate(report.report_date))}
프로젝트: ${escapeHtml(report.project_name || '-')}
공정: ${escapeHtml(report.work_type_name || '-')}
작업: ${escapeHtml(report.task_name || '-')}
작업시간: ${parseFloat(report.total_hours || report.work_hours || 0)}시간
${report.regular_hours !== undefined && report.regular_hours !== null ? `
정규 시간: ${parseFloat(report.regular_hours)}시간
` : ''} ${report.error_hours && report.error_hours > 0 ? `
부적합 처리: ${parseFloat(report.error_hours)}시간
부적합 원인: ${escapeHtml(report.error_type_name || '-')}
` : ''}
작성자: ${escapeHtml(report.created_by_name || '-')}
${report.start_time && report.end_time ? `
작업 시간: ${escapeHtml(report.start_time)} ~ ${escapeHtml(report.end_time)}
` : ''}
`).join(''); container.innerHTML = html; } /** * 작업보고서 수정 모달 열기 */ window.openEditReportModal = function(report) { // 수정 모달이 없으면 동적 생성 let modal = document.getElementById('editReportModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'editReportModal'; modal.className = 'modal-overlay'; modal.style.cssText = 'display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1003; align-items: center; justify-content: center;'; modal.innerHTML = ` `; document.body.appendChild(modal); } // 폼에 데이터 채우기 document.getElementById('editReportId').value = report.id; document.getElementById('editWorkerName').value = report.worker_name || '작업자'; document.getElementById('editProjectId').value = report.project_id || ''; document.getElementById('editWorkHours').value = report.work_hours || report.total_hours || 0; document.getElementById('editWorkStatusId').value = report.work_status_id || 1; // 공정 선택 후 작업 목록 로드 const workTypeSelect = document.getElementById('editWorkTypeId'); // work_type_id가 실제로는 task_id를 저장하고 있으므로, task에서 work_type을 찾아야 함 // 일단 task 기반으로 찾기 시도 loadTasksForEdit().then(() => { const taskSelect = document.getElementById('editTaskId'); // work_type_id 컬럼에 저장된 값이 실제로는 task_id if (report.work_type_id) { taskSelect.value = report.work_type_id; } }); modal.style.display = 'flex'; }; /** * 수정 모달용 작업 목록 로드 */ window.loadTasksForEdit = async function() { const workTypeId = document.getElementById('editWorkTypeId').value; const taskSelect = document.getElementById('editTaskId'); if (!workTypeId) { taskSelect.innerHTML = ''; return; } try { const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`); const tasks = response.data || response || []; taskSelect.innerHTML = '' + tasks.map(t => ``).join(''); } catch (error) { console.error('작업 목록 로드 오류:', error); taskSelect.innerHTML = ''; } }; /** * 수정 모달 닫기 */ window.closeEditReportModal = function() { const modal = document.getElementById('editReportModal'); if (modal) { modal.style.display = 'none'; } }; /** * 수정된 보고서 저장 */ window.saveEditedReport = async function() { const reportId = document.getElementById('editReportId').value; const projectId = document.getElementById('editProjectId').value; const taskId = document.getElementById('editTaskId').value; const workHours = parseFloat(document.getElementById('editWorkHours').value); const workStatusId = document.getElementById('editWorkStatusId').value; if (!projectId || !taskId || !workHours) { showMessage('필수 항목을 모두 입력해주세요.', 'error'); return; } try { const updateData = { project_id: parseInt(projectId), work_type_id: parseInt(taskId), // task_id가 work_type_id 컬럼에 저장됨 work_hours: workHours, work_status_id: parseInt(workStatusId) }; const response = await window.apiCall(`/daily-work-reports/${reportId}`, 'PUT', updateData); if (response.success) { showMessage('작업보고서가 수정되었습니다.', 'success'); closeEditReportModal(); loadCompletedReports(); // 목록 새로고침 } else { throw new Error(response.message || '수정 실패'); } } catch (error) { console.error('작업보고서 수정 오류:', error); showMessage('수정 실패: ' + error.message, 'error'); } }; /** * 작업보고서 삭제 */ window.deleteWorkReport = async function(reportId) { if (!confirm('이 작업보고서를 삭제하시겠습니까?')) { return; } try { const response = await window.apiCall(`/daily-work-reports/${reportId}`, 'DELETE'); if (response.success) { showMessage('작업보고서가 삭제되었습니다.', 'success'); loadCompletedReports(); // 목록 새로고침 } else { throw new Error(response.message || '삭제 실패'); } } catch (error) { console.error('작업보고서 삭제 오류:', error); showMessage('삭제 실패: ' + error.message, 'error'); } }; // ================================================================= // 기존 함수들 // ================================================================= // 한국 시간 기준 오늘 날짜 function getKoreaToday() { if (window.CommonUtils) return window.CommonUtils.getTodayKST(); const now = new Date(); return now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); } // 현재 로그인한 사용자 정보 가져오기 function getCurrentUser() { // SSO 사용자 정보 우선 if (window.getSSOUser) { const ssoUser = window.getSSOUser(); if (ssoUser) return ssoUser; } try { const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token')); if (token) { const payloadBase64 = token.split('.')[1]; if (payloadBase64) { return JSON.parse(atob(payloadBase64)); } } } catch (error) { console.log('토큰에서 사용자 정보 추출 실패:', error); } try { const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('user') || localStorage.getItem('userInfo'); if (userInfo) return JSON.parse(userInfo); } 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 사용)'); // 생산팀 소속 작업자만 조회 const data = await window.apiCall(`/workers?limit=1000&department_id=1`); 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(`/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 response = await window.apiCall(`/daily-work-reports/work-types`); const data = response.data || response; if (Array.isArray(data) && data.length > 0) { workTypes = data; console.log('✅ 작업 유형 API 사용 (통합 설정):', workTypes.length + '개'); return; } throw new Error('API 실패'); } catch (error) { console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용:', error.message); workTypes = [ { id: 1, name: 'Base' }, { id: 2, name: 'Vessel' }, { id: 3, name: 'Piping' } ]; } } async function loadWorkStatusTypes() { try { const response = await window.apiCall(`/daily-work-reports/work-status-types`); const data = response.data || response; if (Array.isArray(data) && data.length > 0) { workStatusTypes = data; console.log('✅ 업무 상태 유형 API 사용 (통합 설정):', workStatusTypes.length + '개'); return; } throw new Error('API 실패'); } catch (error) { console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용'); workStatusTypes = [ { id: 1, name: '정규' }, { id: 2, name: '에러' } ]; } } async function loadErrorTypes() { // 레거시 에러 유형 로드 (호환성) try { const response = await window.apiCall(`/daily-work-reports/error-types`); const data = response.data || response; if (Array.isArray(data) && data.length > 0) { errorTypes = data; } } catch (error) { errorTypes = []; } // 신고 카테고리 로드 (부적합 유형만) try { const catResponse = await window.apiCall('/work-issues/categories/type/nonconformity'); if (catResponse && catResponse.success && Array.isArray(catResponse.data)) { issueCategories = catResponse.data; console.log(`✅ 부적합 카테고리 ${issueCategories.length}개 로드`); // 모든 아이템 로드 const itemResponse = await window.apiCall('/work-issues/items'); if (itemResponse && itemResponse.success && Array.isArray(itemResponse.data)) { // 부적합 카테고리의 아이템만 필터링 const categoryIds = issueCategories.map(c => c.category_id); issueItems = itemResponse.data.filter(item => categoryIds.includes(item.category_id)); console.log(`✅ 부적합 아이템 ${issueItems.length}개 로드`); } } } catch (error) { console.log('⚠️ 신고 카테고리 로드 실패:', error); issueCategories = []; issueItems = []; } } // 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(`/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(`/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 = `

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

`; 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 = escapeHtml(work.project_name || '미지정'); const workTypeName = escapeHtml(work.work_type_name || '미지정'); const workStatusName = escapeHtml(work.work_status_name || '미지정'); const workHours = parseFloat(work.work_hours || 0); const errorTypeName = work.error_type_name ? escapeHtml(work.error_type_name) : null; const workId = parseInt(work.id) || 0; return `
🏗️ 프로젝트
${projectName}
⚙️ 작업종류
${workTypeName}
📊 작업상태
${workStatusName}
⏰ 작업시간
${workHours}시간
${errorTypeName ? `
❌ 에러유형
${errorTypeName}
` : ''}
`; }).join(''); return `
👤 ${escapeHtml(workerName)}
총 ${parseFloat(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(`/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(`/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(`/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 { // app-init.js(defer)가 토큰/apiCall 설정 완료할 때까지 대기 if (window.waitForApi) { await window.waitForApi(8000); } else if (!window.apiCall) { // waitForApi 없으면 간단 폴링 await new Promise((resolve, reject) => { let elapsed = 0; const iv = setInterval(() => { elapsed += 50; if (window.apiCall) { clearInterval(iv); resolve(); } else if (elapsed >= 8000) { clearInterval(iv); reject(new Error('apiCall timeout')); } }, 50); }); } await loadData(); setupEventListeners(); // TBM 작업 목록 로드 (기본 탭) await loadIncompleteTbms(); console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)'); } catch (error) { console.error('초기화 오류:', error); showMessage('초기화 중 오류가 발생했습니다.', 'error'); } } // 페이지 로드 시 초기화 (module 스크립트는 DOMContentLoaded 이후 실행될 수 있음) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { 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, defectIndex, issueReportId } = currentEditingField; // 이슈 기반 부적합 시간 선택인 경우 if (type === 'issueDefect') { if (tempDefects[index] && tempDefects[index][defectIndex] !== undefined) { tempDefects[index][defectIndex].defect_hours = currentTimeValue; // 시간 표시 업데이트 const timeDisplay = document.getElementById(`issueDefectTime_${index}_${issueReportId}`); if (timeDisplay) { timeDisplay.textContent = currentTimeValue; } // 요약 및 hidden 필드 업데이트 updateDefectSummary(index); updateHiddenDefectFields(index); } closeTimePicker(); return; } // 레거시 부적합 시간 선택인 경우 if (type === 'defect') { if (tempDefects[index] && tempDefects[index][defectIndex] !== undefined) { tempDefects[index][defectIndex].defect_hours = currentTimeValue; // 시간 표시 업데이트 const timeDisplay = document.getElementById(`defectTime_${index}_${defectIndex}`); if (timeDisplay) { timeDisplay.textContent = currentTimeValue; } // 요약 및 hidden 필드 업데이트 updateDefectSummary(index); updateHiddenDefectFields(index); } closeTimePicker(); return; } // 기존 total/error 시간 선택 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); }; // ================================================================= // 부적합 원인 관리 (인라인 방식) // ================================================================= /** * 부적합 영역 토글 * - 신고된 이슈 목록 표시 */ window.toggleDefectArea = function(index) { const defectRow = document.getElementById(`defectRow_${index}`); if (!defectRow) return; const isVisible = defectRow.style.display !== 'none'; if (isVisible) { // 숨기기 defectRow.style.display = 'none'; } else { // 보이기 - 부적합 원인이 없으면 자동으로 하나 추가 if (!tempDefects[index] || tempDefects[index].length === 0) { tempDefects[index] = [{ issue_report_id: null, category_id: null, item_id: null, error_type_id: '', // 레거시 호환 defect_hours: 0, note: '' }]; } renderInlineDefectList(index); defectRow.style.display = ''; } }; /** * 인라인 부적합 목록 렌더링 * - 해당 날짜에 신고된 이슈 목록을 표시 * - 이슈 선택 → 시간 입력 방식 */ function renderInlineDefectList(index) { const listContainer = document.getElementById(`defectList_${index}`); if (!listContainer) return; // 해당 TBM의 날짜 가져오기 const tbm = incompleteTbms[index]; const dateStr = tbm ? formatDateForApi(tbm.session_date) : null; const issues = dateStr ? (dailyIssuesCache[dateStr] || []) : []; // 작업장소 정보 가져오기 const workerWorkplaceId = tbm?.workplace_id; const workerWorkplaceName = tbm?.workplace_name; // 부적합 유형 + 작업장소 일치하는 것만 필터링 const nonconformityIssues = issues.filter(i => { // 부적합 유형만 if (i.category_type !== 'nonconformity') return false; // 작업장소 매칭 (workplace_id 우선, 없으면 이름 비교) if (workerWorkplaceId && i.workplace_id) { return i.workplace_id === workerWorkplaceId; } if (workerWorkplaceName && (i.workplace_name || i.custom_location)) { const issueLocation = i.workplace_name || i.custom_location || ''; return issueLocation.includes(workerWorkplaceName) || workerWorkplaceName.includes(issueLocation); } // 작업장소 정보가 없으면 포함하지 않음 return false; }); const defects = tempDefects[index] || []; console.log(`📝 [renderInlineDefectList] index=${index}, 부적합 수=${defects.length}`, defects); // 이슈가 있으면 이슈 선택 UI, 없으면 레거시 UI if (nonconformityIssues.length > 0) { // 이슈 선택 방식 UI let html = `
📋 ${escapeHtml(workerWorkplaceName || '작업장소')} 관련 부적합 ${parseInt(nonconformityIssues.length) || 0}건
`; nonconformityIssues.forEach(issue => { // 이 이슈가 이미 선택되었는지 확인 const existingDefect = defects.find(d => d.issue_report_id == issue.report_id); const isSelected = !!existingDefect; const defectHours = existingDefect?.defect_hours || 0; // 아이템명과 추가설명 조합 let itemText = issue.issue_item_name || ''; if (issue.additional_description) { itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description; } const safeReportId = parseInt(issue.report_id) || 0; html += `
${escapeHtml(issue.issue_category_name || '부적합')} ${escapeHtml(itemText || '-')} ${escapeHtml(issue.workplace_name || issue.custom_location || '')}
${parseFloat(defectHours) || 0} 시간
`; }); html += `
`; // 레거시 방식도 추가 (기타 부적합 추가 버튼) html += `
${renderLegacyDefects(index, defects)}
`; listContainer.innerHTML = html; } else { // 이슈가 없으면 레거시 UI (error_types 선택) const noIssueMsg = workerWorkplaceName ? `${escapeHtml(workerWorkplaceName)}에 신고된 부적합이 없습니다.` : '신고된 부적합이 없습니다.'; listContainer.innerHTML = `
${noIssueMsg}
${renderLegacyDefects(index, defects)}
`; } updateDefectSummary(index); } /** * 레거시 부적합 렌더링 (저장된 항목 + 입력 중인 항목 분리) */ function renderLegacyDefects(index, defects) { // issue_report_id가 없는 직접 입력 부적합 const legacyDefects = defects.filter(d => !d.issue_report_id); if (legacyDefects.length === 0) return ''; // 저장된 항목과 입력 중인 항목 분리 const savedDefects = legacyDefects.filter(d => d._saved); const editingDefects = legacyDefects.filter(d => !d._saved); let html = ''; // 저장된 부적합 표시 (위쪽) if (savedDefects.length > 0) { html += `
저장된 부적합 ${savedDefects.length}건
`; savedDefects.forEach(defect => { const defectIndex = defects.indexOf(defect); const categoryName = defect.category_id ? (issueCategories.find(c => c.category_id == defect.category_id)?.category_name || '미분류') : '미분류'; const itemName = defect.item_id ? (issueItems.find(i => i.item_id == defect.item_id)?.item_name || '') : ''; const displayText = itemName ? `${categoryName} → ${itemName}` : categoryName; html += `
${categoryName} ${itemName}${defect.note ? ' - ' + defect.note : ''}
${defect.defect_hours || 0}시간
`; }); html += `
`; } // 입력 중인 부적합 표시 (아래쪽) if (editingDefects.length > 0) { html += `
`; editingDefects.forEach(defect => { const defectIndex = defects.indexOf(defect); html += renderDefectInputForm(index, defect, defectIndex); }); html += `
`; } return html; } /** * 부적합 입력 폼 렌더링 (단일 항목) */ function renderDefectInputForm(index, defect, defectIndex) { // 대분류 (카테고리) 옵션 const categoryOptions = issueCategories.map(cat => `` ).join(''); // 소분류 (아이템) 옵션 - 선택된 카테고리의 아이템만 const filteredItems = defect.category_id ? issueItems.filter(item => item.category_id == defect.category_id) : []; const itemOptions = filteredItems.map(item => `` ).join(''); // 카테고리명/아이템명 표시 (미리보기용) const categoryName = defect.category_id ? (issueCategories.find(c => c.category_id == defect.category_id)?.category_name || '') : ''; const itemName = defect.item_id ? (issueItems.find(i => i.item_id == defect.item_id)?.item_name || '') : ''; return `
${defect.defect_hours || 0} 시간
${(categoryName || itemName) ? `
${categoryName}${itemName ? ' → ' + itemName : ''}${defect.note ? ' → ' + defect.note : ''}
` : ''}
`; } /** * 대분류 선택 변경 */ window.onDefectCategoryChange = async function(index, defectIndex, value) { if (!tempDefects[index] || !tempDefects[index][defectIndex]) return; const defect = tempDefects[index][defectIndex]; if (value === '__new__') { // 새 카테고리 추가 모달 const newName = prompt('새 대분류(카테고리) 이름을 입력하세요:'); if (newName && newName.trim()) { try { const response = await window.apiCall('/work-issues/categories', 'POST', { category_name: newName.trim(), category_type: 'nonconformity', severity: 'medium' }); if (response.success) { // 카테고리 목록 새로고침 await loadErrorTypes(); // 새로 생성된 카테고리 선택 const newCat = issueCategories.find(c => c.category_name === newName.trim()); if (newCat) { defect.category_id = newCat.category_id; defect.item_id = null; } } else { alert('카테고리 추가 실패: ' + (response.error || '알 수 없는 오류')); defect.category_id = null; } } catch (e) { alert('카테고리 추가 중 오류: ' + e.message); defect.category_id = null; } } else { // 취소 시 이전 값 유지 defect.category_id = defect.category_id || null; } } else if (value) { defect.category_id = parseInt(value); defect.item_id = null; // 카테고리 변경 시 소분류 초기화 } else { defect.category_id = null; defect.item_id = null; } renderInlineDefectList(index); updateHiddenDefectFields(index); }; /** * 소분류 선택 변경 */ window.onDefectItemChange = async function(index, defectIndex, value) { if (!tempDefects[index] || !tempDefects[index][defectIndex]) return; const defect = tempDefects[index][defectIndex]; if (value === '__new__') { // 새 아이템 추가 모달 const newName = prompt('새 소분류(항목) 이름을 입력하세요:'); if (newName && newName.trim() && defect.category_id) { try { const response = await window.apiCall('/work-issues/items', 'POST', { category_id: defect.category_id, item_name: newName.trim(), severity: 'medium' }); if (response.success) { // 아이템 목록 새로고침 await loadErrorTypes(); // 새로 생성된 아이템 선택 const newItem = issueItems.find(i => i.item_name === newName.trim() && i.category_id == defect.category_id); if (newItem) { defect.item_id = newItem.item_id; } } else { alert('항목 추가 실패: ' + (response.error || '알 수 없는 오류')); } } catch (e) { alert('항목 추가 중 오류: ' + e.message); } } } else if (value) { defect.item_id = parseInt(value); } else { defect.item_id = null; } renderInlineDefectList(index); updateHiddenDefectFields(index); }; /** * 이슈 부적합 토글 (체크박스) */ window.toggleIssueDefect = function(index, issueReportId, isChecked) { if (!tempDefects[index]) { tempDefects[index] = []; } if (isChecked) { // 이슈 부적합 추가 tempDefects[index].push({ issue_report_id: issueReportId, error_type_id: null, // 이슈 기반이므로 null defect_hours: 0, note: '' }); } else { // 이슈 부적합 제거 const idx = tempDefects[index].findIndex(d => d.issue_report_id == issueReportId); if (idx !== -1) { tempDefects[index].splice(idx, 1); } } renderInlineDefectList(index); updateHiddenDefectFields(index); }; /** * 이슈 부적합 시간 선택기 열기 */ window.openIssueDefectTimePicker = function(index, issueReportId) { // 해당 이슈의 defect 찾기 const defects = tempDefects[index] || []; const defectIndex = defects.findIndex(d => d.issue_report_id == issueReportId); if (defectIndex === -1) return; currentEditingField = { index, type: 'issueDefect', issueReportId, defectIndex }; currentTimeValue = defects[defectIndex]?.defect_hours || 0; // 팝오버 표시 const overlay = document.getElementById('timePickerOverlay'); const title = document.getElementById('timePickerTitle'); title.textContent = '부적합 시간 선택'; updateTimeDisplay(); overlay.style.display = 'flex'; // ESC 키로 닫기 document.addEventListener('keydown', handleEscapeKey); }; /** * 레거시 부적합 추가 (error_types 기반) */ window.addLegacyDefect = function(index) { if (!tempDefects[index]) { tempDefects[index] = []; } tempDefects[index].push({ issue_report_id: null, category_id: null, item_id: null, error_type_id: '', // 레거시 호환 defect_hours: 0, note: '' }); renderInlineDefectList(index); }; /** * 부적합 카테고리/아이템 선택 업데이트 (레거시 호환) * @deprecated onDefectCategoryChange, onDefectItemChange 사용 권장 */ window.updateDefectCategory = function(index, defectIndex, value) { // 레거시 호환 - 새 함수로 리다이렉트 if (value && value.startsWith('cat_')) { onDefectCategoryChange(index, defectIndex, value.replace('cat_', '')); } else if (value && value.startsWith('item_')) { const itemId = parseInt(value.replace('item_', '')); const item = issueItems.find(i => i.item_id === itemId); if (item) { if (!tempDefects[index]?.[defectIndex]) return; tempDefects[index][defectIndex].category_id = item.category_id; tempDefects[index][defectIndex].item_id = itemId; renderInlineDefectList(index); updateHiddenDefectFields(index); } } }; /** * 인라인 부적합 추가 (레거시 호환) */ window.addInlineDefect = function(index) { addLegacyDefect(index); }; /** * 부적합 저장 확인 (유효성 검사 후 저장 상태로 변경) */ window.saveDefectsConfirm = function(index) { const defects = tempDefects[index] || []; // 입력 중인 항목만 (저장되지 않은 항목) const editingDefects = defects.filter(d => !d.issue_report_id && !d._saved); if (editingDefects.length === 0) { alert('저장할 부적합 항목이 없습니다.\n"+ 부적합 추가" 버튼을 눌러 항목을 추가하세요.'); return; } // 유효성 검사 const invalidDefects = []; editingDefects.forEach((defect, i) => { const errors = []; if (!defect.category_id) { errors.push('대분류'); } if (!defect.defect_hours || defect.defect_hours <= 0) { errors.push('시간'); } if (errors.length > 0) { invalidDefects.push({ index: i + 1, errors }); } }); if (invalidDefects.length > 0) { const errorMsg = invalidDefects.map(d => `${d.index}번째 항목: ${d.errors.join(', ')} 미입력` ).join('\n'); alert(`입력이 완료되지 않은 항목이 있습니다.\n\n${errorMsg}`); return; } // 모든 입력 중인 항목을 저장 상태로 변경 editingDefects.forEach(defect => { defect._saved = true; }); // UI 다시 렌더링 renderInlineDefectList(index); updateHiddenDefectFields(index); updateDefectSummary(index); console.log(`[부적합 저장] index=${index}, 저장된 항목 수=${editingDefects.length}`); }; /** * 저장된 부적합 수정 (저장 상태 해제하여 입력 폼으로 이동) */ window.editSavedDefect = function(index, defectIndex) { if (!tempDefects[index] || !tempDefects[index][defectIndex]) return; // 원래 저장 상태였음을 기록 (취소 시 복원용) tempDefects[index][defectIndex]._originalSaved = true; tempDefects[index][defectIndex]._saved = false; // UI 다시 렌더링 renderInlineDefectList(index); }; /** * 저장된 부적합 삭제 */ window.deleteSavedDefect = function(index, defectIndex) { if (!tempDefects[index] || !tempDefects[index][defectIndex]) return; if (!confirm('이 부적합 항목을 삭제하시겠습니까?')) return; tempDefects[index].splice(defectIndex, 1); // UI 다시 렌더링 renderInlineDefectList(index); updateHiddenDefectFields(index); updateDefectSummary(index); }; /** * 부적합 입력 취소 (저장되지 않은 항목 삭제) */ window.cancelDefectEdit = function(index, defectIndex) { if (!tempDefects[index] || !tempDefects[index][defectIndex]) return; const defect = tempDefects[index][defectIndex]; // 원래 저장된 항목이었으면 저장 상태로 복원, 아니면 삭제 if (defect._originalSaved) { defect._saved = true; delete defect._originalSaved; } else { tempDefects[index].splice(defectIndex, 1); } // UI 다시 렌더링 renderInlineDefectList(index); updateHiddenDefectFields(index); updateDefectSummary(index); }; /** * 인라인 부적합 수정 */ window.updateInlineDefect = function(index, defectIndex, field, value) { if (tempDefects[index] && tempDefects[index][defectIndex]) { if (field === 'defect_hours') { tempDefects[index][defectIndex][field] = parseFloat(value) || 0; } else { tempDefects[index][defectIndex][field] = value; } updateDefectSummary(index); updateHiddenDefectFields(index); } }; /** * 인라인 부적합 삭제 */ window.removeInlineDefect = function(index, defectIndex) { if (tempDefects[index]) { tempDefects[index].splice(defectIndex, 1); // UI 다시 렌더링 (항상 - 빈 상태도 표시) renderInlineDefectList(index); updateDefectSummary(index); updateHiddenDefectFields(index); } }; /** * 부적합 시간 선택기 열기 (시간 선택 팝오버 재사용) */ window.openDefectTimePicker = function(index, defectIndex) { currentEditingField = { index, type: 'defect', defectIndex }; // 현재 값 가져오기 const defects = tempDefects[index] || []; currentTimeValue = defects[defectIndex]?.defect_hours || 0; // 팝오버 표시 const overlay = document.getElementById('timePickerOverlay'); const title = document.getElementById('timePickerTitle'); title.textContent = '부적합 시간 선택'; updateTimeDisplay(); overlay.style.display = 'flex'; // ESC 키로 닫기 document.addEventListener('keydown', handleEscapeKey); }; /** * hidden input 필드 업데이트 */ function updateHiddenDefectFields(index) { const defects = tempDefects[index] || []; // 총 부적합 시간 계산 const totalErrorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0); // hidden input에 대표 error_type_id 저장 (첫 번째 값, item_id fallback) const errorTypeInput = document.getElementById(`errorType_${index}`); const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null; if (errorTypeInput && errorTypeId) { errorTypeInput.value = errorTypeId; } else if (errorTypeInput) { errorTypeInput.value = ''; } // 부적합 시간 input 업데이트 const errorHoursInput = document.getElementById(`errorHours_${index}`); if (errorHoursInput) { errorHoursInput.value = totalErrorHours; } } /** * 부적합 요약 텍스트 업데이트 * - 이슈 기반 부적합과 레거시 부적합 모두 포함 */ function updateDefectSummary(index) { const summaryEl = document.getElementById(`defectSummary_${index}`); const toggleBtn = document.getElementById(`defectToggle_${index}`); if (!summaryEl) return; const defects = tempDefects[index] || []; // 이슈 기반 또는 레거시 부적합 중 시간이 입력된 것만 유효 (item_id도 체크) const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0); if (validDefects.length === 0) { summaryEl.textContent = '없음'; summaryEl.style.color = '#6b7280'; if (toggleBtn) toggleBtn.classList.remove('has-defect'); } else { const totalHours = validDefects.reduce((sum, d) => sum + d.defect_hours, 0); if (validDefects.length === 1) { let typeName = '부적합'; if (validDefects[0].issue_report_id) { // 이슈 기반 - 이슈 캐시에서 이름 찾기 const tbm = incompleteTbms[index]; if (tbm) { const dateStr = formatDateForApi(tbm.session_date); const issues = dailyIssuesCache[dateStr] || []; const issue = issues.find(i => i.report_id == validDefects[0].issue_report_id); typeName = issue?.issue_item_name || issue?.issue_category_name || '부적합'; } } else if (validDefects[0].item_id) { // 신규 방식 - issue_report_items에서 이름 찾기 typeName = issueItems.find(i => i.item_id == validDefects[0].item_id)?.item_name || '부적합'; } else if (validDefects[0].category_id) { // 카테고리만 선택된 경우 typeName = issueCategories.find(c => c.category_id == validDefects[0].category_id)?.category_name || '부적합'; } else if (validDefects[0].error_type_id) { // 레거시 - error_types에서 이름 찾기 또는 issue_report_items에서 찾기 typeName = issueItems.find(i => i.item_id == validDefects[0].error_type_id)?.item_name || errorTypes.find(et => et.id == validDefects[0].error_type_id)?.name || '부적합'; } summaryEl.textContent = `${typeName} ${totalHours}h`; } else { summaryEl.textContent = `${validDefects.length}건 ${totalHours}h`; } summaryEl.style.color = '#dc2626'; if (toggleBtn) toggleBtn.classList.add('has-defect'); } // hidden 필드도 업데이트 updateHiddenDefectFields(index); }