// ✅ modern-dashboard.js - 모던 대시보드 JavaScript // API 설정 및 함수들은 api-config.js에서 로드됨 // window.API, window.apiCall, window.getAuthHeaders 사용 // 인증 관련 함수들 function getAuthData() { const token = localStorage.getItem('token'); const user = localStorage.getItem('user'); return { token, user: user ? JSON.parse(user) : null }; } // 전역 변수 let currentUser = null; let workersData = []; let workData = []; let selectedDate = new Date().toISOString().split('T')[0]; // 모달 관련 변수 let currentModalWorker = null; let modalWorkTypes = []; let modalWorkStatusTypes = []; let modalErrorTypes = []; let modalProjects = []; let modalExistingWork = []; // DOM 요소 const elements = { currentTime: document.getElementById('currentTime'), timeValue: document.getElementById('timeValue'), userName: document.getElementById('userName'), userRole: document.getElementById('userRole'), userInitial: document.getElementById('userInitial'), selectedDate: document.getElementById('selectedDate'), // 작업장 현황으로 교체되어 없을 수 있음 refreshBtn: document.getElementById('refreshBtn'), // 작업장 현황으로 교체되어 없을 수 있음 logoutBtn: document.getElementById('logoutBtn'), // 요약 카드 todayWorkers: document.getElementById('todayWorkers'), totalHours: document.getElementById('totalHours'), activeProjects: document.getElementById('activeProjects'), errorCount: document.getElementById('errorCount'), // 컨테이너 workStatusContainer: document.getElementById('workStatusContainer'), // 작업장 현황으로 교체되어 없을 수 있음 workersContainer: document.getElementById('workersContainer'), toastContainer: document.getElementById('toastContainer') }; // ========== 초기화 ========== // document.addEventListener('DOMContentLoaded', async () => { // API 함수가 로드될 때까지 기다림 let retryCount = 0; const maxRetries = 50; // 5초 대기 while (!window.apiCall && retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, 100)); retryCount++; } if (!window.apiCall) { console.error('❌ API 함수를 로드할 수 없습니다.'); showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error'); return; } try { await initializeDashboard(); } catch (error) { console.error('대시보드 초기화 오류:', error); showToast('대시보드를 불러오는 중 오류가 발생했습니다.', 'error'); } }); async function initializeDashboard() { console.log('🚀 모던 대시보드 초기화 시작'); // 사용자 정보 설정 setupUserInfo(); // 시간 업데이트 시작 updateCurrentTime(); setInterval(updateCurrentTime, 1000); // 날짜 설정 (요소가 있을 때만) if (elements.selectedDate) { elements.selectedDate.value = selectedDate; } // 이벤트 리스너 설정 setupEventListeners(); // 데이터 로드 (작업 현황 컨테이너가 있을 때만) if (elements.workStatusContainer) { await loadDashboardData(); } // 관리자 권한 확인 checkAdminAccess(); // TBM 페이지 접근 권한 확인 checkTbmPageAccess(); console.log('✅ 모던 대시보드 초기화 완료'); } // ========== 사용자 정보 설정 ========== // // navbar/sidebar는 app-init.js에서 공통 처리하므로 여기서는 currentUser만 설정 function setupUserInfo() { const authData = getAuthData(); if (authData && authData.user) { currentUser = authData.user; console.log('👤 사용자 정보 로드 완료:', currentUser.name, currentUser.role); } } // ========== 시간 업데이트 ========== // function updateCurrentTime() { // Navbar 컴포넌트가 시간을 처리하므로 여기서는 timeValue가 있을 때만 업데이트 if (elements.timeValue) { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); elements.timeValue.textContent = `${hours}시 ${minutes}분 ${seconds}초`; } } // ========== 이벤트 리스너 ========== // function setupEventListeners() { // 날짜 변경 (요소가 있을 때만) if (elements.selectedDate) { elements.selectedDate.addEventListener('change', (e) => { selectedDate = e.target.value; loadDashboardData(); }); } // 새로고침 버튼 (요소가 있을 때만) if (elements.refreshBtn) { elements.refreshBtn.addEventListener('click', () => { loadDashboardData(); showToast('데이터를 새로고침했습니다.', 'success'); }); } // 로그아웃 버튼 (navbar 컴포넌트가 이미 처리하므로 버튼이 있을 때만) if (elements.logoutBtn) { elements.logoutBtn.addEventListener('click', () => { if (confirm('로그아웃하시겠습니까?')) { localStorage.clear(); window.location.href = '/index.html'; } }); } // 뷰 컨트롤 버튼들 const listViewBtn = document.getElementById('listViewBtn'); const cardViewBtn = document.getElementById('cardViewBtn'); if (listViewBtn) { listViewBtn.addEventListener('click', () => { displayWorkers(workersData, 'list'); updateViewButtons('list'); }); } if (cardViewBtn) { cardViewBtn.addEventListener('click', () => { displayWorkers(workersData, 'card'); updateViewButtons('card'); }); } } // ========== 데이터 로드 ========== // async function loadDashboardData() { console.log('📊 대시보드 데이터 로딩 시작'); try { // 로딩 상태 표시 showLoadingState(); // 병렬로 데이터 로드 const [workersResult, workResult] = await Promise.all([ loadWorkers(), loadWorkData(selectedDate) ]); // 요약 데이터 업데이트 updateSummaryCards(); // 작업 현황 표시 displayWorkStatus(); // 작업자 현황 표시 displayWorkers(workersData, 'card'); console.log('✅ 대시보드 데이터 로딩 완료'); } catch (error) { console.error('❌ 대시보드 데이터 로딩 오류:', error); showErrorState(); showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); } } async function loadWorkers() { try { console.log('👥 작업자 데이터 로딩...'); const response = await window.apiCall('/workers'); const allWorkers = Array.isArray(response) ? response : (response.data || []); // 활성화된 작업자만 필터링 workersData = allWorkers.filter(worker => { return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true; }); console.log(`✅ 작업자 ${workersData.length}명 로드 완료 (전체: ${allWorkers.length}명)`); return workersData; } catch (error) { console.error('작업자 데이터 로딩 오류:', error); workersData = []; throw error; } } async function loadWorkData(date) { try { console.log(`📋 ${date} 작업 데이터 로딩...`); const response = await window.apiCall(`/daily-work-reports?date=${date}&view_all=true`); workData = Array.isArray(response) ? response : (response.data || []); console.log(`✅ 작업 데이터 ${workData.length}건 로드 완료`); return workData; } catch (error) { console.error('작업 데이터 로딩 오류:', error); workData = []; throw error; } } // ========== 요약 카드 업데이트 ========== // function updateSummaryCards() { // 오늘 작업자 수 const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size; updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명'); // 총 작업 시간 const totalHours = workData.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); updateSummaryCard(elements.totalHours, totalHours.toFixed(1), '시간'); // 진행 중인 프로젝트 const activeProjectsCount = new Set(workData.map(w => w.project_id)).size; updateSummaryCard(elements.activeProjects, activeProjectsCount, '개'); // 오류 발생 건수 const errorCount = workData.filter(w => w.work_status_id === 2).length; updateSummaryCard(elements.errorCount, errorCount, '건'); } function updateSummaryCard(element, value, unit) { if (element) { const numberElement = element.querySelector('.value-number'); const unitElement = element.querySelector('.value-unit'); if (numberElement) numberElement.textContent = value; if (unitElement) unitElement.textContent = unit; } } // ========== SVG 아이콘 정의 ========== // const SVG_ICONS = { complete: ` `, overtime: ` `, vacation: ` `, partial: ` `, incomplete: ` `, warning: ` ` }; // ========== 작업 현황 표시 (작업자 중심) ========== // function displayWorkStatus() { const tableBody = document.getElementById('workStatusTableBody'); if (!tableBody) return; // 모든 작업자 데이터 가져오기 (작업이 없는 작업자도 포함) const allWorkers = workersData || []; if (allWorkers.length === 0) { tableBody.innerHTML = `

등록된 작업자가 없습니다

`; return; } // 작업자별 상황 분석 const workerStatusList = allWorkers.map(worker => { const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); // 휴가/연차 제외한 실제 작업시간 계산 const actualWorkHours = todayWork .filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외 .reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const hasError = todayWork.some(w => w.work_status_id === 2); // 정규 작업과 에러 작업 건수 분리 const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length; const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length; // 상태 판단 로직 (개선된 버전) let status = 'incomplete'; let statusText = '미입력'; let statusBadge = '미입력'; let statusClass = 'incomplete'; let vacationType = null; // 휴가 처리된 경우 확인 (프로젝트 ID 13 = "연차/휴무" 또는 설명에 휴가 키워드) const hasVacationRecord = todayWork.some(w => w.project_id === 13 || // 연차/휴무 프로젝트 (w.description && ( w.description.includes('연차') || w.description.includes('반차') || w.description.includes('휴가') )) ); // 연차/휴무 프로젝트의 시간 계산 const vacationHours = todayWork .filter(w => w.project_id === 13) .reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); if (totalHours > 12) { status = 'overtime-warning'; statusText = '초과근무 확인필요'; statusBadge = '확인필요'; statusClass = 'warning'; } else if (hasVacationRecord && vacationHours > 0) { // 연차/휴무 시간에 따른 상태 결정 if (vacationHours === 8) { status = 'vacation-full'; statusText = '연차'; statusBadge = '연차'; statusClass = 'vacation'; } else if (vacationHours === 6) { status = 'vacation-half-half'; statusText = '조퇴'; statusBadge = '조퇴'; statusClass = 'vacation'; } else if (vacationHours === 4) { status = 'vacation-half'; statusText = '반차'; statusBadge = '반차'; statusClass = 'vacation'; } else if (vacationHours === 2) { status = 'vacation-quarter'; statusText = '반반차'; statusBadge = '반반차'; statusClass = 'vacation'; } } else if (totalHours > 8) { // 8시간 초과 - 연장근로 status = 'overtime'; statusText = '연장근로'; statusBadge = '연장근로'; statusClass = 'overtime'; } else if (totalHours === 8) { // 정확히 8시간 - 정시근로 status = 'complete'; statusText = '정시근로'; statusBadge = '정시근로'; statusClass = 'success'; } else if (totalHours > 0) { // 0시간 초과 8시간 미만 - 부분 입력 status = 'partial'; statusText = '부분 입력'; statusBadge = '부분입력'; statusClass = 'info'; // 휴가 처리 필요 여부 판단 if (totalHours === 0) { vacationType = 'full'; } else if (totalHours === 4) { vacationType = 'half'; } else if (totalHours === 6) { vacationType = 'half-half'; // 2시간 더 추가해서 조퇴 처리 } } else { // 0시간 - 미입력 status = 'incomplete'; statusText = '미입력'; statusBadge = '미입력'; statusClass = 'incomplete'; vacationType = 'full'; } return { ...worker, todayWork, totalHours, actualWorkHours, regularWorkCount, errorWorkCount, hasError, status, statusText, statusBadge, statusClass, vacationType }; }); // 테이블 행 렌더링 tableBody.innerHTML = workerStatusList.map(worker => { // 상태에 따른 SVG 아이콘 선택 let iconKey = 'incomplete'; if (worker.status === 'overtime-warning') iconKey = 'warning'; else if (worker.status.startsWith('vacation')) iconKey = 'vacation'; else if (worker.status === 'overtime') iconKey = 'overtime'; else if (worker.status === 'complete') iconKey = 'complete'; else if (worker.status === 'partial') iconKey = 'partial'; return `
${worker.worker_name.charAt(0)}
${worker.worker_name}
${worker.job_type || '작업자'}
${SVG_ICONS[iconKey]} ${worker.statusBadge} ${worker.actualWorkHours.toFixed(1)}h
정규: ${worker.regularWorkCount}건 ${worker.errorWorkCount > 0 ? ` 에러: ${worker.errorWorkCount}건 ` : ''}
${worker.vacationType ? ` ` : ''} ${worker.status === 'overtime-warning' ? ` ` : ''}
`; }).join(''); } function groupWorkDataByProject() { const groups = {}; workData.forEach(work => { const projectName = work.project_name || '미지정 프로젝트'; if (!groups[projectName]) { groups[projectName] = []; } groups[projectName].push(work); }); return groups; } // ========== 작업자 현황 표시 ========== // function displayWorkers(workers, viewType = 'card') { if (!elements.workersContainer) return; if (workers.length === 0) { elements.workersContainer.innerHTML = `
👥

작업자 데이터가 없습니다

등록된 작업자가 없습니다.

`; return; } if (viewType === 'list') { displayWorkersAsList(workers); } else { displayWorkersAsCards(workers); } } function displayWorkersAsCards(workers) { elements.workersContainer.innerHTML = `
${workers.map(worker => { const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); // 휴가/연차 제외한 실제 작업시간 계산 const actualWorkHours = todayWork .filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외 .reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const hasError = todayWork.some(w => w.work_status_id === 2); // 정규 작업과 에러 작업 건수 분리 const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length; const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length; return `
${worker.worker_name.charAt(0)}

${worker.worker_name}

${worker.job_type || '작업자'}

작업시간 ${actualWorkHours.toFixed(1)}h
정규 ${regularWorkCount}건
${errorWorkCount > 0 ? `
에러 ${errorWorkCount}건
` : ''}
`; }).join('')}
`; } function displayWorkersAsList(workers) { elements.workersContainer.innerHTML = `
${workers.map(worker => { const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const hasError = todayWork.some(w => w.work_status_id === 2); return ` `; }).join('')}
작업자 직종 오늘 작업 작업 시간 상태
${worker.worker_name.charAt(0)}
${worker.worker_name}
${worker.job_type || '작업자'} ${todayWork.length}건 ${totalHours.toFixed(1)}시간 ${todayWork.length > 0 ? '작업 중' : '대기'} ${hasError ? '오류' : ''}
`; } // ========== 뷰 버튼 업데이트 ========== // function updateViewButtons(activeView) { const listBtn = document.getElementById('listViewBtn'); const cardBtn = document.getElementById('cardViewBtn'); if (listBtn && cardBtn) { listBtn.classList.toggle('btn-primary', activeView === 'list'); listBtn.classList.toggle('btn-secondary', activeView !== 'list'); cardBtn.classList.toggle('btn-primary', activeView === 'card'); cardBtn.classList.toggle('btn-secondary', activeView !== 'card'); } } // ========== 관리자 권한 확인 ========== // function checkAdminAccess() { const adminElements = document.querySelectorAll('.admin-only'); const isFullAdmin = currentUser && ['admin', 'system'].includes(currentUser.access_level); const isGroupLeader = currentUser && currentUser.access_level === 'group_leader'; console.log(`🔐 권한 확인: 사용자=${currentUser?.username}, 역할=${currentUser.access_level}, 전체관리자=${isFullAdmin}, 그룹리더=${isGroupLeader}`); adminElements.forEach(element => { const href = element.getAttribute('href'); // 작업 분석: 전체 관리자만 접근 가능 if (href && href.includes('work-analysis.html')) { if (isFullAdmin) { element.style.display = ''; element.classList.add('visible'); } else { element.style.display = 'none'; element.classList.remove('visible'); } } // 작업 관리: 전체 관리자 + 그룹 리더 접근 가능 else if (href && href.includes('work-management.html')) { if (isFullAdmin || isGroupLeader) { element.style.display = ''; element.classList.add('visible'); } else { element.style.display = 'none'; element.classList.remove('visible'); } } // 기타 관리자 전용 메뉴: 전체 관리자만 접근 가능 else { if (isFullAdmin) { element.style.display = ''; element.classList.add('visible'); } else { element.style.display = 'none'; element.classList.remove('visible'); } } }); } // ========== TBM 페이지 접근 권한 확인 ========== // async function checkTbmPageAccess() { try { if (!currentUser || !currentUser.user_id) { console.log('⚠️ TBM 페이지 권한 확인: 사용자 정보 없음'); return; } const tbmQuickAction = document.getElementById('tbmQuickAction'); if (!tbmQuickAction) { console.log('⚠️ TBM 빠른 작업 버튼 요소를 찾을 수 없습니다'); return; } console.log('🛠️ TBM 페이지 권한 확인 중...', { role: currentUser.role, access_level: currentUser.access_level }); // Admin은 모든 페이지 접근 가능 if (currentUser.role === 'Admin' || currentUser.role === 'System Admin' || currentUser.access_level === 'admin' || currentUser.access_level === 'system') { tbmQuickAction.style.display = 'block'; console.log('✅ Admin 사용자 - TBM 빠른 작업 버튼 표시'); return; } // 일반 사용자는 페이지 접근 권한 조회 const response = await window.apiCall(`/users/${currentUser.user_id}/page-access`); if (response && response.success) { const pageAccess = response.data?.pageAccess || []; // 'work.tbm' 페이지 접근 권한 확인 (마이그레이션에서 work.tbm으로 등록함) const tbmPage = pageAccess.find(p => p.page_key === 'work.tbm'); if (tbmPage && tbmPage.can_access) { tbmQuickAction.style.display = 'block'; console.log('✅ TBM 페이지 접근 권한 있음 - 빠른 작업 버튼 표시'); } else { console.log('❌ TBM 페이지 접근 권한 없음 - 빠른 작업 버튼 숨김'); } } else { console.log('⚠️ TBM 페이지 권한 확인 실패'); } } catch (error) { console.error('❌ TBM 페이지 권한 확인 오류:', error); } } // ========== 상태 표시 ========== // function showLoadingState() { const loadingHTML = `

데이터를 불러오는 중...

`; if (elements.workStatusContainer) { elements.workStatusContainer.innerHTML = loadingHTML; } if (elements.workersContainer) { elements.workersContainer.innerHTML = loadingHTML; } } function showErrorState() { const errorHTML = `
⚠️

데이터를 불러올 수 없습니다

네트워크 연결을 확인하고 다시 시도해주세요.

`; if (elements.workStatusContainer) { elements.workStatusContainer.innerHTML = errorHTML; } if (elements.workersContainer) { elements.workersContainer.innerHTML = errorHTML; } } // ========== 토스트 알림 ========== // function showToast(message, type = 'info', duration = 3000) { if (!elements.toastContainer) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; const iconMap = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; toast.innerHTML = `
${iconMap[type] || 'ℹ️'}
${message}
`; elements.toastContainer.appendChild(toast); // 자동 제거 setTimeout(() => { if (toast.parentElement) { toast.remove(); } }, duration); } // ========== 작업자 관련 액션 함수들 ========== // function openWorkerModal(workerId, workerName) { console.log(`📝 ${workerName}(ID: ${workerId}) 작업 보고서 모달 열기`); // 모달 데이터 설정 currentModalWorker = { id: workerId, name: workerName, date: selectedDate }; // 모달 표시 showWorkerModal(); } function handleVacation(workerId, vacationType) { console.log(`🏖️ 작업자 ${workerId} 휴가 처리: ${vacationType}`); const vacationNames = { 'full': '연차', 'half': '반차', 'half-half': '반반차' }; const vacationHours = { 'full': 8, 'half': 4, 'half-half': 2 }; if (confirm(`${vacationNames[vacationType]} 처리하시겠습니까?\n(${vacationHours[vacationType]}시간으로 자동 입력됩니다)`)) { // 휴가 처리 API 호출 processVacation(workerId, vacationType, vacationHours[vacationType]); } } async function processVacation(workerId, vacationType, hours) { try { showToast(`휴가 처리 중...`, 'info'); // 휴가용 작업 보고서 생성 (특별한 작업 유형으로) const vacationReport = { report_date: selectedDate, worker_id: workerId, project_id: 1, // 기본 프로젝트 (휴가용) work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요) work_status_id: 1, // 정상 상태 error_type_id: null, work_hours: hours, created_by: currentUser?.user_id || 1 }; const response = await window.apiCall('/daily-work-reports', 'POST', vacationReport); showToast(`휴가 처리가 완료되었습니다.`, 'success'); await loadDashboardData(); // 데이터 새로고침 } catch (error) { console.error('휴가 처리 오류:', error); showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error'); } } function confirmOvertime(workerId) { console.log(`⚠️ 작업자 ${workerId} 초과근무 확인`); if (confirm('12시간을 초과한 작업시간이 정상적인 입력인지 확인하시겠습니까?')) { // 초과근무 확인 처리 processOvertimeConfirmation(workerId); } } async function processOvertimeConfirmation(workerId) { try { showToast('초과근무 승인 처리 중...', 'info'); // 새로운 근태 관리 API 사용 const overtimeData = { worker_id: workerId, date: selectedDate }; const response = await window.apiCall('/attendance/overtime/approve', 'POST', overtimeData); if (response.success) { showToast('초과근무가 정상으로 승인되었습니다.', 'success'); await loadDashboardData(); // 데이터 새로고침 } else { throw new Error(response.message || '초과근무 승인에 실패했습니다.'); } } catch (error) { console.error('초과근무 승인 오류:', error); showToast(`초과근무 승인 중 오류가 발생했습니다: ${error.message}`, 'error'); } } // ========== 모달 시스템 ========== // function showWorkerModal() { // 모달이 없으면 생성 if (!document.getElementById('workerModal')) { createWorkerModal(); } // 모달 데이터 로드 및 표시 loadModalData(); document.getElementById('workerModal').style.display = 'flex'; document.body.style.overflow = 'hidden'; // 배경 스크롤 방지 } function hideWorkerModal() { document.getElementById('workerModal').style.display = 'none'; document.body.style.overflow = 'auto'; // 배경 스크롤 복원 resetModalForm(); } function createWorkerModal() { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); setupModalEventListeners(); } function setupModalEventListeners() { // 모달 외부 클릭 시 닫기 document.getElementById('workerModal').addEventListener('click', (e) => { if (e.target.id === 'workerModal') { hideWorkerModal(); } }); // 새 작업 추가/취소 버튼 document.getElementById('modalAddWorkBtn').addEventListener('click', showModalNewWorkForm); document.getElementById('modalCancelWorkBtn').addEventListener('click', hideModalNewWorkForm); document.getElementById('modalSaveWorkBtn').addEventListener('click', saveModalNewWork); // 업무 상태 변경 시 에러 유형 토글 document.getElementById('modalWorkStatusSelect').addEventListener('change', toggleModalErrorType); // 빠른 시간 버튼 document.querySelectorAll('.modal-time-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.getElementById('modalWorkHours').value = e.target.dataset.hours; }); }); // 휴가 처리 버튼 document.querySelectorAll('.modal-vacation-btn').forEach(btn => { btn.addEventListener('click', (e) => { const vacationType = e.target.dataset.type; handleModalVacation(vacationType); }); }); } async function loadModalData() { if (!currentModalWorker) return; try { // 모달 헤더 업데이트 document.getElementById('modalTitle').textContent = `${currentModalWorker.name} 작업 보고서`; document.getElementById('modalWorkerName').textContent = currentModalWorker.name; document.getElementById('modalWorkerDate').textContent = currentModalWorker.date; document.getElementById('modalWorkerInitial').textContent = currentModalWorker.name.charAt(0); // 병렬로 데이터 로드 await Promise.all([ loadModalExistingWork(), loadModalDropdownData() ]); // UI 업데이트 updateModalSummary(); renderModalExistingWork(); populateModalDropdowns(); } catch (error) { console.error('모달 데이터 로드 오류:', error); showToast('데이터 로드 중 오류가 발생했습니다.', 'error'); } } async function loadModalExistingWork() { try { const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`); modalExistingWork = Array.isArray(response) ? response : (response.data || []); } catch (error) { console.error('기존 작업 로드 오류:', error); modalExistingWork = []; } } async function loadModalDropdownData() { try { const [projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([ window.apiCall('/projects/active/list'), window.apiCall('/daily-work-reports/work-types'), window.apiCall('/daily-work-reports/work-status-types'), window.apiCall('/daily-work-reports/error-types') ]); modalProjects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []); modalWorkTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []); modalWorkStatusTypes = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []); modalErrorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []); } catch (error) { console.error('드롭다운 데이터 로드 오류:', error); } } function updateModalSummary() { const totalHours = modalExistingWork.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); const workCount = modalExistingWork.length; document.getElementById('modalTotalHours').textContent = `${totalHours.toFixed(1)}h`; document.getElementById('modalWorkCount').textContent = `${workCount}건`; // 12시간 초과 경고 if (totalHours > 12) { document.getElementById('modalTotalHours').classList.add('warning'); } else { document.getElementById('modalTotalHours').classList.remove('warning'); } } function renderModalExistingWork() { const container = document.getElementById('modalExistingWork'); if (modalExistingWork.length === 0) { container.innerHTML = ` `; return; } container.innerHTML = modalExistingWork.map(work => ` `).join(''); } function populateModalDropdowns() { // 프로젝트 드롭다운 const projectSelect = document.getElementById('modalProjectSelect'); projectSelect.innerHTML = ''; modalProjects.forEach(project => { projectSelect.innerHTML += ``; }); // 작업 유형 드롭다운 const workTypeSelect = document.getElementById('modalWorkTypeSelect'); workTypeSelect.innerHTML = ''; modalWorkTypes.forEach(type => { workTypeSelect.innerHTML += ``; }); // 작업 상태 드롭다운 const workStatusSelect = document.getElementById('modalWorkStatusSelect'); workStatusSelect.innerHTML = ''; modalWorkStatusTypes.forEach(status => { workStatusSelect.innerHTML += ``; }); // 에러 유형 드롭다운 const errorTypeSelect = document.getElementById('modalErrorTypeSelect'); errorTypeSelect.innerHTML = ''; modalErrorTypes.forEach(error => { errorTypeSelect.innerHTML += ``; }); } function showModalNewWorkForm() { document.getElementById('modalNewWorkSection').style.display = 'block'; document.getElementById('modalAddWorkBtn').style.display = 'none'; } function hideModalNewWorkForm() { document.getElementById('modalNewWorkSection').style.display = 'none'; document.getElementById('modalAddWorkBtn').style.display = 'block'; resetModalForm(); } function resetModalForm() { document.getElementById('modalProjectSelect').value = ''; document.getElementById('modalWorkTypeSelect').value = ''; document.getElementById('modalWorkStatusSelect').value = ''; document.getElementById('modalErrorTypeSelect').value = ''; document.getElementById('modalWorkHours').value = '1.00'; document.getElementById('modalErrorTypeGroup').style.display = 'none'; } function toggleModalErrorType() { const workStatusSelect = document.getElementById('modalWorkStatusSelect'); const errorTypeGroup = document.getElementById('modalErrorTypeGroup'); if (workStatusSelect.value === '2') { // 에러 상태 errorTypeGroup.style.display = 'block'; } else { errorTypeGroup.style.display = 'none'; document.getElementById('modalErrorTypeSelect').value = ''; } } async function saveModalNewWork() { try { const projectId = document.getElementById('modalProjectSelect').value; const workTypeId = document.getElementById('modalWorkTypeSelect').value; const workStatusId = document.getElementById('modalWorkStatusSelect').value; const errorTypeId = document.getElementById('modalErrorTypeSelect').value; const workHours = document.getElementById('modalWorkHours').value; // 유효성 검사 if (!projectId || !workTypeId || !workStatusId || !workHours) { showToast('모든 필수 필드를 입력해주세요.', 'error'); return; } if (workStatusId === '2' && !errorTypeId) { showToast('에러 상태일 때는 에러 유형을 선택해야 합니다.', 'error'); return; } const workData = { report_date: currentModalWorker.date, worker_id: currentModalWorker.id, work_entries: [{ project_id: parseInt(projectId), task_id: parseInt(workTypeId), // work_type_id를 task_id로 매핑 work_hours: parseFloat(workHours), work_status_id: parseInt(workStatusId), error_type_id: workStatusId === '2' ? parseInt(errorTypeId) : null, description: '' // 기본 설명 }] }; console.log('📤 전송할 작업 데이터:', workData); console.log('📋 현재 사용자:', currentUser); await window.apiCall('/daily-work-reports', 'POST', workData); showToast('작업이 성공적으로 저장되었습니다.', 'success'); // 데이터 새로고침 await loadModalExistingWork(); updateModalSummary(); renderModalExistingWork(); hideModalNewWorkForm(); // 대시보드 데이터도 새로고침 await loadDashboardData(); } catch (error) { console.error('작업 저장 오류:', error); showToast(`작업 저장 중 오류가 발생했습니다: ${error.message}`, 'error'); } } async function deleteModalWork(workId) { if (!confirm('이 작업을 삭제하시겠습니까?')) { return; } try { await window.apiCall(`/daily-work-reports/${workId}`, 'DELETE'); showToast('작업이 성공적으로 삭제되었습니다.', 'success'); // 데이터 새로고침 await loadModalExistingWork(); updateModalSummary(); renderModalExistingWork(); // 대시보드 데이터도 새로고침 await loadDashboardData(); } catch (error) { console.error('작업 삭제 오류:', error); showToast(`작업 삭제 중 오류가 발생했습니다: ${error.message}`, 'error'); } } async function handleModalVacation(vacationType) { const vacationTypeMap = { 'full': { code: 'ANNUAL_FULL', name: '연차', hours: 8 }, 'half': { code: 'ANNUAL_HALF', name: '반차', hours: 4 }, 'half-half': { code: 'ANNUAL_QUARTER', name: '반반차', hours: 2 } }; const vacation = vacationTypeMap[vacationType]; if (!vacation) return; if (!confirm(`${vacation.name} 처리하시겠습니까?\n(${vacation.hours}시간으로 자동 입력됩니다)`)) { return; } try { // 새로운 근태 관리 API 사용 const vacationData = { worker_id: currentModalWorker.id, date: currentModalWorker.date, vacation_type: vacation.code }; const response = await window.apiCall('/attendance/vacation', 'POST', vacationData); if (response.success) { showToast(`${vacation.name} 처리가 완료되었습니다.`, 'success'); // 데이터 새로고침 await loadModalExistingWork(); updateModalSummary(); renderModalExistingWork(); // 대시보드 데이터도 새로고침 await loadDashboardData(); } else { throw new Error(response.message || '휴가 처리에 실패했습니다.'); } } catch (error) { console.error('휴가 처리 오류:', error); showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error'); } } // ========== 전역 함수 (HTML에서 호출) ========== // window.loadDashboardData = loadDashboardData; window.showToast = showToast; window.updateSummaryCards = updateSummaryCards; window.displayWorkers = displayWorkers; window.openWorkerModal = openWorkerModal; window.hideWorkerModal = hideWorkerModal; window.deleteModalWork = deleteModalWork; window.handleVacation = handleVacation; window.confirmOvertime = confirmOvertime;