// 작업 현황 캘린더 JavaScript // 전역 변수 대신 CalendarState 사용 // let currentDate = new Date(); // let monthlyData = {}; // 월별 데이터 캐시 // let allWorkers = []; // 작업자 데이터는 allWorkers 변수 사용 // let currentModalDate = null; // let currentEditingWork = null; // let existingWorks = []; // DOM 요소 const elements = { monthYearTitle: null, calendarDays: null, prevMonthBtn: null, nextMonthBtn: null, todayBtn: null, dailyWorkModal: null, modalTitle: null, modalTotalWorkers: null, modalTotalHours: null, modalTotalTasks: null, modalErrorCount: null, modalWorkersList: null, statusFilter: null, loadingSpinner: null }; // 초기화 document.addEventListener('DOMContentLoaded', async function() { console.log('🚀 작업 현황 캘린더 초기화 시작'); // DOM 요소 초기화 initializeElements(); // 이벤트 리스너 등록 setupEventListeners(); // 작업자 데이터 로드 (한 번만) await loadWorkersData(); // 현재 월 캘린더 렌더링 await renderCalendar(); console.log('✅ 작업 현황 캘린더 초기화 완료'); }); // DOM 요소 초기화 function initializeElements() { elements.monthYearTitle = document.getElementById('monthYearTitle'); elements.calendarDays = document.getElementById('calendarDays'); elements.prevMonthBtn = document.getElementById('prevMonthBtn'); elements.nextMonthBtn = document.getElementById('nextMonthBtn'); elements.todayBtn = document.getElementById('todayBtn'); elements.dailyWorkModal = document.getElementById('dailyWorkModal'); elements.modalTitle = document.getElementById('modalTitle'); elements.modalSummary = document.querySelector('.daily-summary'); // 요약 섹션 elements.modalTotalWorkers = document.getElementById('modalTotalWorkers'); elements.modalTotalHours = document.getElementById('modalTotalHours'); elements.modalTotalTasks = document.getElementById('modalTotalTasks'); elements.modalErrorCount = document.getElementById('modalErrorCount'); elements.modalWorkersList = document.getElementById('modalWorkersList'); elements.modalNoData = document.getElementById('modalNoData'); elements.statusFilter = document.getElementById('statusFilter'); elements.loadingSpinner = document.getElementById('loadingSpinner'); } // 이벤트 리스너 설정 function setupEventListeners() { elements.prevMonthBtn.addEventListener('click', () => { CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() - 1); renderCalendar(); }); elements.nextMonthBtn.addEventListener('click', () => { CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() + 1); renderCalendar(); }); elements.todayBtn.addEventListener('click', () => { CalendarState.currentDate = new Date(); renderCalendar(); }); elements.statusFilter.addEventListener('change', filterWorkersList); // 모달 외부 클릭 시 닫기 elements.dailyWorkModal.addEventListener('click', (e) => { if (e.target === elements.dailyWorkModal) { closeDailyWorkModal(); } }); // ESC 키로 모달 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && elements.dailyWorkModal.style.display !== 'none') { closeDailyWorkModal(); } }); } // 작업자 데이터 로드 (캐시) async function loadWorkersData() { if (CalendarState.allWorkers.length > 0) return CalendarState.allWorkers; try { console.log('👥 작업자 데이터 로딩 (from CalendarAPI)...'); // The new API function already filters for active workers const activeWorkers = await CalendarAPI.getWorkers(); CalendarState.allWorkers = activeWorkers; console.log(`✅ 작업자 ${CalendarState.allWorkers.length}명 로드 완료`); return CalendarState.allWorkers; } catch (error) { console.error('작업자 데이터 로딩 오류:', error); showToast(error.message, 'error'); return []; } } // 월별 작업 데이터 로드 (집계 테이블 사용으로 최적화) async function loadMonthlyWorkData(year, month) { const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`; if (CalendarState.monthlyData[monthKey]) { console.log(`📋 캐시된 ${monthKey} 데이터 사용`); return CalendarState.monthlyData[monthKey]; } try { const data = await CalendarAPI.getMonthlyCalendarData(year, month); CalendarState.monthlyData[monthKey] = data; // Cache the data return data; } catch (error) { console.error(`${monthKey} 데이터 로딩 오류:`, error); showToast(error.message, 'error'); return {}; // Return empty object on failure } } // 일일 작업 현황 모달 열기 async function openDailyWorkModal(dateStr) { console.log(`🗓️ 클릭된 날짜: ${dateStr}`); CalendarState.currentModalDate = dateStr; // 날짜 포맷팅 const date = new Date(dateStr + 'T00:00:00'); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); console.log(`📅 파싱된 날짜: ${year}년 ${month}월 ${day}일`); const dayNames = ['일', '월', '화', '수', '목', '금', '토']; const dayName = dayNames[date.getDay()]; elements.modalTitle.textContent = `${year}년 ${month}월 ${day}일 (${dayName}) 작업 현황`; try { const response = await CalendarAPI.getDailyDetails(dateStr); if (response.workers) { // New API structure renderModalDataFromSummary(response.workers, response.summary); } else { // Fallback structure renderModalData(response); } // 모달 표시 elements.dailyWorkModal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } catch (error) { console.error('일일 작업 데이터 로딩 오류:', error); showToast('해당 날짜의 작업 데이터를 불러오는데 실패했습니다.', 'error'); } } // 집계 데이터로 모달 렌더링 (최적화된 버전) async function renderModalDataFromSummary(workers, summary) { // 전체 작업자 목록 가져오기 const allWorkersList = await loadWorkersData(); // 작업한 작업자 ID 목록 const workedWorkerIds = new Set(workers.map(w => w.workerId)); // 미기입 작업자 추가 (대시보드와 동일한 상태 판단 로직 적용) const missingWorkers = allWorkersList .filter(worker => !workedWorkerIds.has(worker.worker_id)) .map(worker => { return { workerId: worker.worker_id, workerName: worker.worker_name, jobType: worker.job_type, totalHours: 0, actualWorkHours: 0, vacationHours: 0, totalWorkCount: 0, regularWorkCount: 0, errorWorkCount: 0, status: 'incomplete', hasVacation: false, hasError: false, hasIssues: true }; }); // 전체 작업자 목록 (작업한 사람 + 미기입 사람) const allModalWorkers = [...workers, ...missingWorkers]; // 요약 정보 업데이트 (전체 작업자 수 포함) if (elements.modalTotalWorkers) { elements.modalTotalWorkers.textContent = `${allModalWorkers.length}명`; } if (elements.modalTotalHours) { elements.modalTotalHours.textContent = `${summary.totalHours.toFixed(1)}h`; } if (elements.modalTotalTasks) { elements.modalTotalTasks.textContent = `${summary.totalTasks}건`; } if (elements.modalErrorCount) { elements.modalErrorCount.textContent = `${summary.errorCount}건`; elements.modalErrorCount.className = summary.errorCount > 0 ? 'summary-value error' : 'summary-value'; } // 작업자 리스트 렌더링 if (allModalWorkers.length === 0) { elements.modalWorkersList.innerHTML = '
등록된 작업자가 없습니다.
'; return; } const workersHtml = allModalWorkers.map(worker => { // 상태 텍스트 및 색상 결정 (에러가 있어도 작업시간 기준으로 판단) let statusText = '미입력'; let statusClass = 'incomplete'; // 에러 여부와 관계없이 작업시간 기준으로 상태 결정 const totalHours = worker.totalHours || 0; const hasVacation = worker.hasVacation || false; const vacationHours = worker.vacationHours || 0; if (totalHours > 12) { statusText = '확인필요'; statusClass = 'overtime-warning'; } else if (hasVacation && vacationHours > 0) { switch (vacationHours) { case 8: statusText = '연차'; statusClass = 'vacation-full'; break; case 6: statusText = '조퇴'; statusClass = 'vacation-half-half'; break; case 4: statusText = '반차'; statusClass = 'vacation-half'; break; case 2: statusText = '반반차'; statusClass = 'vacation-quarter'; break; default: statusText = '연차'; statusClass = 'vacation-full'; } } else if (totalHours > 8) { statusText = '연장근로'; statusClass = 'overtime'; } else if (totalHours === 8) { statusText = '정시근로'; statusClass = 'complete'; } else if (totalHours > 0) { statusText = '부분입력'; statusClass = 'partial'; } else { statusText = '미입력'; statusClass = 'incomplete'; } // 작업자 이름의 첫 글자 추출 const initial = worker.workerName ? worker.workerName.charAt(0) : '?'; // 관리자/그룹장 권한 확인 const currentUser = JSON.parse(localStorage.getItem('user') || '{}'); const isAdmin = ['admin', 'system', 'group_leader'].includes(currentUser.access_level || currentUser.role); // 삭제 버튼 (관리자/그룹장만 표시, 작업이 있는 경우에만) const deleteBtn = isAdmin && worker.totalWorkCount > 0 ? ` ` : ''; return `
${initial}
${worker.workerName}
${worker.jobType || '일반'}
${statusText}
작업시간 ${worker.actualWorkHours.toFixed(1)}h
정규 ${worker.regularWorkCount}건 에러 ${worker.errorWorkCount}건
${deleteBtn}
`; }).join(''); elements.modalWorkersList.innerHTML = workersHtml; } // 모달 데이터 렌더링 (폴백용 - 기존 방식) function renderModalData(workData) { // 작업자별로 그룹화 const workerGroups = {}; workData.forEach(work => { if (!workerGroups[work.worker_id]) { workerGroups[work.worker_id] = { worker_id: work.worker_id, worker_name: work.worker_name, job_type: work.job_type, works: [] }; } workerGroups[work.worker_id].works.push(work); }); // 요약 정보 계산 const totalWorkers = Object.keys(workerGroups).length; const totalHours = workData.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const totalTasks = workData.length; const errorCount = workData.filter(w => w.work_status_id === 2).length; // 요약 정보 업데이트 elements.modalTotalWorkers.textContent = `${totalWorkers}명`; elements.modalTotalHours.textContent = `${totalHours.toFixed(1)}h`; elements.modalTotalTasks.textContent = `${totalTasks}건`; elements.modalErrorCount.textContent = `${errorCount}건`; // 작업자 리스트 렌더링 renderWorkersList(Object.values(workerGroups)); } // 작업자 리스트 렌더링 function renderWorkersList(workerGroups) { const workersHTML = workerGroups.map(workerGroup => { const totalHours = workerGroup.works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const hasError = workerGroup.works.some(w => w.work_status_id === 2); const hasVacation = workerGroup.works.some(w => w.project_id === 13); const regularWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length; const errorWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id === 2).length; // 상태 결정 let status, statusText, statusBadge; if (hasVacation) { const vacationHours = workerGroup.works .filter(w => w.project_id === 13) .reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); if (vacationHours === 8) { status = 'vacation-full'; statusText = '연차'; statusBadge = '연차'; } else if (vacationHours === 6) { status = 'vacation-half-half'; statusText = '조퇴'; statusBadge = '조퇴'; } else if (vacationHours === 4) { status = 'vacation-half'; statusText = '반차'; statusBadge = '반차'; } else if (vacationHours === 2) { status = 'vacation-quarter'; statusText = '반반차'; statusBadge = '반반차'; } } else if (totalHours > 8) { status = 'overtime'; statusText = '연장근로'; statusBadge = '연장근로'; } else if (totalHours === 8) { status = 'complete'; statusText = '정시근로'; statusBadge = '정시근로'; } else if (totalHours > 0) { status = 'partial'; statusText = '부분입력'; statusBadge = '부분입력'; } else { status = 'incomplete'; statusText = '미입력'; statusBadge = '미입력'; } return `
${workerGroup.worker_name.charAt(0)}

${workerGroup.worker_name}

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

${statusBadge}
작업시간 ${totalHours.toFixed(1)}h
정규 ${regularWorkCount}건
${errorWorkCount > 0 ? `
에러 ${errorWorkCount}건
` : ''}
`; }).join(''); elements.modalWorkersList.innerHTML = workersHTML; } // 작업자 리스트 필터링 function filterWorkersList() { const filterValue = elements.statusFilter.value; const workerRows = elements.modalWorkersList.querySelectorAll('.worker-status-row'); workerRows.forEach(row => { const status = row.dataset.status; if (filterValue === 'all' || status === filterValue || (filterValue === 'vacation' && status.startsWith('vacation'))) { row.style.display = 'flex'; } else { row.style.display = 'none'; } }); } // 모달 닫기 function closeDailyWorkModal() { elements.dailyWorkModal.style.display = 'none'; document.body.style.overflow = ''; CalendarState.currentModalDate = null; } // 로딩 표시 function showLoading(show) { if (elements.loadingSpinner) { elements.loadingSpinner.style.display = show ? 'flex' : 'none'; } } // 토스트 메시지 (간단한 구현) function showToast(message, type = 'info') { // 기존 토스트가 있으면 제거 const existingToast = document.querySelector('.toast-message'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = `toast-message toast-${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 24px; background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'}; color: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; font-weight: 500; max-width: 400px; `; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } // 작업자의 해당 날짜 작업 전체 삭제 (관리자/그룹장용) async function deleteWorkerDayWork(workerId, date, workerName) { // 확인 대화상자 const confirmed = confirm( `⚠️ 정말로 삭제하시겠습니까?\n\n` + `작업자: ${workerName}\n` + `날짜: ${date}\n\n` + `이 작업자의 해당 날짜 모든 작업이 삭제됩니다.\n` + `삭제된 작업은 복구할 수 없습니다.` ); if (!confirmed) return; try { showToast('작업을 삭제하는 중...', 'info'); // 날짜+작업자별 전체 삭제 API 호출 const result = await CalendarAPI.deleteWorkerDayWork(workerId, date); console.log('✅ 작업 삭제 성공:', result); showToast(`${workerName}의 ${date} 작업이 삭제되었습니다.`, 'success'); // 모달 데이터 새로고침 await openDailyWorkModal(CalendarState.currentModalDate); // 캘린더도 새로고침 await renderCalendar(); } catch (error) { console.error('❌ 작업 삭제 실패:', error); showToast(`작업 삭제 실패: ${error.message}`, 'error'); } } // 작업자 개별 작업 모달 열기 async function openWorkerModal(workerId, date) { try { // 작업자 정보 찾기 const worker = CalendarState.allWorkers.find(w => w.worker_id === workerId); if (!worker) { showToast('작업자 정보를 찾을 수 없습니다.', 'error'); return; } // 작업 입력 모달 열기 await openWorkEntryModal(workerId, worker.worker_name, date); } catch (error) { console.error('작업자 모달 열기 오류:', error); showToast('작업 입력 모달을 여는데 실패했습니다.', 'error'); } } // 작업 입력 모달 열기 async function openWorkEntryModal(workerId, workerName, date) { try { // 모달 요소들 가져오기 const modal = document.getElementById('workEntryModal'); const titleElement = document.getElementById('workEntryModalTitle'); const workerNameDisplay = document.getElementById('workerNameDisplay'); const workerIdInput = document.getElementById('workerId'); const workDateInput = document.getElementById('workDate'); if (!modal) { showToast('작업 입력 모달을 찾을 수 없습니다.', 'error'); return; } // 모달 제목 및 정보 설정 titleElement.textContent = `${workerName} - 작업 관리`; workerNameDisplay.value = workerName; workerIdInput.value = workerId; workDateInput.value = date; // 기존 작업 데이터 로드 await loadExistingWorks(workerId, date); // 프로젝트 및 상태 데이터 로드 await loadModalData(); // 기본적으로 기존 작업 탭 활성화 switchTab('existing'); // 모달 표시 modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } catch (error) { console.error('작업 입력 모달 열기 오류:', error); showToast('작업 입력 모달을 여는데 실패했습니다.', 'error'); } } // 모달 데이터 로드 (프로젝트, 작업 상태) async function loadModalData() { try { // 활성 프로젝트 목록 로드 const projectsResponse = await window.apiCall('/projects/active/list'); const projects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []); const projectSelect = document.getElementById('projectSelect'); projectSelect.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.project_id; option.textContent = project.project_name; projectSelect.appendChild(option); }); // 작업 상태 목록 로드 (하드코딩으로 대체) const statuses = [ { status_id: 1, status_name: '완료' }, { status_id: 2, status_name: '오류' }, { status_id: 3, status_name: '진행중' } ]; const statusSelect = document.getElementById('workStatusSelect'); statusSelect.innerHTML = ''; statuses.forEach(status => { const option = document.createElement('option'); option.value = status.status_id; option.textContent = status.status_name; statusSelect.appendChild(option); }); } catch (error) { console.error('모달 데이터 로드 오류:', error); showToast('데이터를 불러오는데 실패했습니다.', 'error'); } } // 작업 입력 모달 닫기 function closeWorkEntryModal() { const modal = document.getElementById('workEntryModal'); if (modal) { modal.style.display = 'none'; document.body.style.overflow = 'auto'; // 폼 초기화 const form = document.getElementById('workEntryForm'); if (form) { form.reset(); } } } // 휴가 처리 function handleVacation(type) { const projectSelect = document.getElementById('projectSelect'); const workHours = document.getElementById('workHours'); const workStatusSelect = document.getElementById('workStatusSelect'); const workDescription = document.getElementById('workDescription'); // 연차/휴무 프로젝트 선택 (project_id: 13) projectSelect.value = '13'; // 휴가 유형에 따른 시간 설정 switch (type) { case 'full': // 연차 workHours.value = '8'; workDescription.value = '연차'; break; case 'half': // 반차 workHours.value = '4'; workDescription.value = '반차'; break; case 'quarter': // 반반차 workHours.value = '2'; workDescription.value = '반반차'; break; case 'early': // 조퇴 workHours.value = '6'; workDescription.value = '조퇴'; break; } // 완료 상태로 설정 (status_id: 1) workStatusSelect.value = '1'; } // 작업 저장 async function saveWorkEntry() { try { const form = document.getElementById('workEntryForm'); const formData = new FormData(form); const workData = { worker_id: document.getElementById('workerId').value, project_id: document.getElementById('projectSelect').value, work_type_id: document.getElementById('workTypeSelect').value, // 추가된 필드 work_hours: document.getElementById('workHours').value, work_status_id: document.getElementById('workStatusSelect').value, error_type_id: document.getElementById('errorTypeSelect')?.value || null, // 추가된 필드 description: document.getElementById('workDescription').value, report_date: document.getElementById('workDate').value }; const editingWorkId = document.getElementById('editingWorkId').value; // 필수 필드 검증 if (!workData.project_id || !workData.work_type_id || !workData.work_hours || !workData.work_status_id) { showToast('필수 항목을 모두 입력해주세요.', 'error'); return; } // API 호출 (수정 또는 신규) let response; if (editingWorkId) { // 수정 모드 - 서버가 기대하는 형태로 데이터 변환 const updateData = { project_id: workData.project_id, work_type_id: workData.work_type_id, // 실제 테이블 컬럼명 사용 work_hours: workData.work_hours, work_status_id: workData.work_status_id, // 실제 테이블 컬럼명 사용 error_type_id: workData.error_type_id // 실제 테이블 컬럼명 사용 }; console.log('🔄 수정용 서버로 전송할 데이터:', updateData); response = await window.apiCall(`/daily-work-reports/${editingWorkId}`, 'PUT', updateData); } else { // 신규 추가 모드 - 서버가 기대하는 형태로 데이터 변환 const serverData = { report_date: workData.report_date, worker_id: workData.worker_id, work_entries: [{ project_id: workData.project_id, task_id: workData.work_type_id, // work_type_id를 task_id로 매핑 work_hours: workData.work_hours, work_status_id: workData.work_status_id, error_type_id: workData.error_type_id, description: workData.description }] }; console.log('🔄 서버로 전송할 데이터:', serverData); response = await window.apiCall('/daily-work-reports', 'POST', serverData); } if (response.success || response.id) { const action = editingWorkId ? '수정' : '저장'; showToast(`작업이 성공적으로 ${action}되었습니다.`, 'success'); // 기존 작업 목록 새로고침 await loadExistingWorks(workData.worker_id, workData.report_date); // 기존 작업 탭으로 전환 switchTab('existing'); // 캘린더 새로고침 await renderCalendar(); // 현재 열린 모달이 있다면 새로고침 if (CalendarState.currentModalDate) { await openDailyWorkModal(CalendarState.currentModalDate); } } else { const action = editingWorkId ? '수정' : '저장'; throw new Error(response.message || `${action}에 실패했습니다.`); } } catch (error) { console.error('작업 저장 오류:', error); showToast(error.message || '작업 저장에 실패했습니다.', 'error'); } } // 모달 닫기 함수 function closeDailyWorkModal() { if (elements.dailyWorkModal) { elements.dailyWorkModal.style.display = 'none'; document.body.style.overflow = 'auto'; } } // 전역 변수로 작업자 목록 저장 // let allWorkers = []; // Now in CalendarState // 시간 업데이트 함수 function updateCurrentTime() { const now = new Date(); const timeString = now.toLocaleTimeString('ko-KR', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeValueElement = document.getElementById('timeValue'); if (timeValueElement) { timeValueElement.textContent = timeString; } } // 사용자 정보 업데이트 함수 function updateUserInfo() { // auth-check.js에서 사용하는 'user' 키와 기존 'userInfo' 키 모두 확인 let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}'); let authUser = JSON.parse(localStorage.getItem('user') || '{}'); console.log('👤 localStorage userInfo:', userInfo); console.log('👤 localStorage user (auth):', authUser); // 두 소스에서 사용자 정보 통합 const finalUserInfo = { worker_name: userInfo.worker_name || authUser.username || authUser.worker_name, job_type: userInfo.job_type || authUser.role || authUser.job_type, username: authUser.username || userInfo.username }; console.log('👤 최종 사용자 정보:', finalUserInfo); const userNameElement = document.getElementById('userName'); const userRoleElement = document.getElementById('userRole'); const userInitialElement = document.getElementById('userInitial'); if (userNameElement) { if (finalUserInfo.worker_name) { userNameElement.textContent = finalUserInfo.worker_name; } else { userNameElement.textContent = '사용자'; } } if (userRoleElement) { if (finalUserInfo.job_type) { // role을 한글로 변환 const roleMap = { 'leader': '그룹장', 'worker': '작업자', 'admin': '관리자' }; userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type; } else { userRoleElement.textContent = '작업자'; } } if (userInitialElement) { if (finalUserInfo.worker_name) { userInitialElement.textContent = finalUserInfo.worker_name.charAt(0); } else { userInitialElement.textContent = '사'; } } } // 페이지 초기화 개선 function initializePage() { // 시간 업데이트 시작 updateCurrentTime(); setInterval(updateCurrentTime, 1000); // 사용자 정보 업데이트 updateUserInfo(); // 프로필 메뉴 토글 const userProfile = document.getElementById('userProfile'); const profileMenu = document.getElementById('profileMenu'); if (userProfile && profileMenu) { userProfile.addEventListener('click', (e) => { e.stopPropagation(); profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block'; }); // 외부 클릭 시 메뉴 닫기 document.addEventListener('click', () => { profileMenu.style.display = 'none'; }); } // 로그아웃 버튼 const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', () => { localStorage.removeItem('token'); localStorage.removeItem('userInfo'); window.location.href = '/pages/auth/login.html'; }); } } // DOMContentLoaded 이벤트에 초기화 함수 추가 document.addEventListener('DOMContentLoaded', function() { initializePage(); }); // ========== 작업 입력 모달 개선 기능들 ========== // 탭 전환 함수 function switchTab(tabName) { // 모든 탭 버튼 비활성화 document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); // 모든 탭 콘텐츠 숨기기 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // 선택된 탭 활성화 const selectedTabBtn = document.querySelector(`[data-tab="${tabName}"]`); const selectedTabContent = document.getElementById(`${tabName}WorkTab`); if (selectedTabBtn) selectedTabBtn.classList.add('active'); if (selectedTabContent) selectedTabContent.classList.add('active'); // 새 작업 탭으로 전환 시 폼 초기화 if (tabName === 'new') { resetWorkForm(); } } // 기존 작업 데이터 로드 async function loadExistingWorks(workerId, date) { try { console.log(`📋 기존 작업 로드: 작업자 ${workerId}, 날짜 ${date}`); let workerWorks = []; try { // 방법 1: 날짜별 작업 보고서 조회 시도 const response = await apiCall(`/daily-work-reports/date/${date}`, 'GET'); if (response && Array.isArray(response)) { console.log(`📊 방법1 - 전체 응답 데이터 (${response.length}건):`, response); // 김두수(작업자 ID 1)의 모든 작업 확인 const allWorkerOneWorks = response.filter(work => work.worker_id == 1); console.log(`🔍 김두수(ID=1)의 모든 작업 (${allWorkerOneWorks.length}건):`, allWorkerOneWorks); // 해당 작업자의 작업만 필터링 workerWorks = response.filter(work => { const isMatch = work.worker_id == workerId; console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`); return isMatch; }); console.log(`✅ 방법1 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`); console.log('📋 필터링된 작업 목록:', workerWorks); } } catch (dateApiError) { console.warn('📅 날짜별 API 실패, 범위 조회 시도:', dateApiError.message); try { // 방법 2: 범위 조회로 fallback (해당 날짜만) const response = await apiCall(`/daily-work-reports?start=${date}&end=${date}`, 'GET'); if (response && Array.isArray(response)) { console.log(`📊 방법2 - 전체 응답 데이터 (${response.length}건):`, response); workerWorks = response.filter(work => { const isMatch = work.worker_id == workerId; console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`); return isMatch; }); console.log(`✅ 방법2 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`); console.log('📋 필터링된 작업 목록:', workerWorks); } } catch (rangeApiError) { console.warn('📊 범위 조회도 실패:', rangeApiError.message); // 최종적으로 빈 배열로 처리 workerWorks = []; } } CalendarState.existingWorks = workerWorks; renderExistingWorks(); updateTabCounter(); } catch (error) { console.error('기존 작업 로드 오류:', error); CalendarState.existingWorks = []; renderExistingWorks(); updateTabCounter(); } } // 기존 작업 목록 렌더링 function renderExistingWorks() { console.log('🎨 작업 목록 렌더링 시작:', CalendarState.existingWorks); const existingWorkList = document.getElementById('existingWorkList'); const noExistingWork = document.getElementById('noExistingWork'); const totalWorkCount = document.getElementById('totalWorkCount'); const totalWorkHours = document.getElementById('totalWorkHours'); if (!existingWorkList) { console.error('❌ existingWorkList 요소를 찾을 수 없습니다.'); return; } // 총 작업 시간 계산 const totalHours = CalendarState.existingWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); console.log(`📊 작업 통계: ${CalendarState.existingWorks.length}건, 총 ${totalHours}시간`); // 요약 정보 업데이트 if (totalWorkCount) totalWorkCount.textContent = CalendarState.existingWorks.length; if (totalWorkHours) totalWorkHours.textContent = totalHours.toFixed(1); if (CalendarState.existingWorks.length === 0) { existingWorkList.style.display = 'none'; if (noExistingWork) noExistingWork.style.display = 'block'; console.log('ℹ️ 작업이 없어서 빈 상태 표시'); return; } existingWorkList.style.display = 'block'; if (noExistingWork) noExistingWork.style.display = 'none'; // 각 작업 데이터 상세 로그 CalendarState.existingWorks.forEach((work, index) => { console.log(`📋 작업 ${index + 1}:`, { id: work.id, project_name: work.project_name, work_hours: work.work_hours, work_status_name: work.work_status_name, created_at: work.created_at, description: work.description }); }); // 작업 목록 HTML 생성 const worksHtml = CalendarState.existingWorks.map((work, index) => { const workItemHtml = `
${work.project_name || '프로젝트 정보 없음'}
⏰ ${work.work_hours}시간 📊 ${work.work_status_name || '상태 정보 없음'} 📅 ${new Date(work.created_at).toLocaleString('ko-KR')}
${work.description ? `
${work.description}
` : ''}
`; console.log(`🏗️ 작업 ${index + 1} HTML 생성 완료`); return workItemHtml; }).join(''); console.log(`📝 최종 HTML 길이: ${worksHtml.length} 문자`); console.log('🎯 HTML 내용 미리보기:', worksHtml.substring(0, 200) + '...'); existingWorkList.innerHTML = worksHtml; // 렌더링 후 실제 DOM 요소 확인 const renderedItems = existingWorkList.querySelectorAll('.work-item'); console.log(`✅ 렌더링 완료: ${renderedItems.length}개 작업 아이템이 DOM에 추가됨`); if (renderedItems.length !== CalendarState.existingWorks.length) { console.error(`⚠️ 렌더링 불일치: 데이터 ${CalendarState.existingWorks.length}건 vs DOM ${renderedItems.length}개`); } } // 탭 카운터 업데이트 function updateTabCounter() { const existingTabBtn = document.querySelector('[data-tab="existing"]'); if (existingTabBtn) { existingTabBtn.innerHTML = `📋 기존 작업 (${CalendarState.existingWorks.length}건)`; } } // 작업 수정 function editWork(workId) { const work = CalendarState.existingWorks.find(w => w.id === workId); if (!work) { showToast('작업 정보를 찾을 수 없습니다.', 'error'); return; } // 수정 모드로 전환 CalendarState.currentEditingWork = work; // 새 작업 탭으로 전환 switchTab('new'); // 폼에 기존 데이터 채우기 document.getElementById('editingWorkId').value = work.id; document.getElementById('projectSelect').value = work.project_id; document.getElementById('workHours').value = work.work_hours; document.getElementById('workStatusSelect').value = work.work_status_id; document.getElementById('workDescription').value = work.description || ''; // UI 업데이트 document.getElementById('workContentTitle').textContent = '작업 내용 수정'; document.getElementById('saveWorkBtn').innerHTML = '💾 수정 완료'; document.getElementById('deleteWorkBtn').style.display = 'inline-block'; // 휴가 섹션 숨기기 (수정 시에는 휴가 처리 불가) document.getElementById('vacationSection').style.display = 'none'; } // 작업 삭제 확인 function confirmDeleteWork(workId) { const work = CalendarState.existingWorks.find(w => w.id === workId); if (!work) { showToast('작업 정보를 찾을 수 없습니다.', 'error'); return; } if (confirm(`"${work.project_name}" 작업을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 작업은 복구할 수 없습니다.`)) { deleteWorkById(workId); } } // 작업 삭제 실행 async function deleteWorkById(workId) { try { const response = await apiCall(`/daily-work-reports/${workId}`, 'DELETE'); if (response.success) { showToast('작업이 성공적으로 삭제되었습니다.', 'success'); // 기존 작업 목록 새로고침 const workerId = document.getElementById('workerId').value; const date = document.getElementById('workDate').value; await loadExistingWorks(workerId, date); // 현재 열린 모달이 있다면 새로고침 if (CalendarState.currentModalDate) { await openDailyWorkModal(CalendarState.currentModalDate); } } else { showToast(response.message || '작업 삭제에 실패했습니다.', 'error'); } } catch (error) { console.error('작업 삭제 오류:', error); showToast('작업 삭제 중 오류가 발생했습니다.', 'error'); } } // 작업 폼 초기화 function resetWorkForm() { CalendarState.currentEditingWork = null; // 폼 필드 초기화 document.getElementById('editingWorkId').value = ''; document.getElementById('projectSelect').value = ''; document.getElementById('workHours').value = ''; document.getElementById('workStatusSelect').value = ''; document.getElementById('workDescription').value = ''; // UI 초기화 document.getElementById('workContentTitle').textContent = '작업 내용'; document.getElementById('saveWorkBtn').innerHTML = '💾 저장'; document.getElementById('deleteWorkBtn').style.display = 'none'; document.getElementById('vacationSection').style.display = 'block'; } // 작업 삭제 (수정 모드에서) function deleteWork() { if (CalendarState.currentEditingWork) { confirmDeleteWork(CalendarState.currentEditingWork.id); } } // 휴가 처리 함수 function handleVacation(vacationType) { const workHours = document.getElementById('workHours'); const projectSelect = document.getElementById('projectSelect'); const workTypeSelect = document.getElementById('workTypeSelect'); const workStatusSelect = document.getElementById('workStatusSelect'); const errorTypeSelect = document.getElementById('errorTypeSelect'); const workDescription = document.getElementById('workDescription'); // 휴가 시간 설정 const vacationHours = { 'full': 8, // 연차 'half': 4, // 반차 'quarter': 2, // 반반차 'early': 6 // 조퇴 }; const vacationNames = { 'full': '연차', 'half': '반차', 'quarter': '반반차', 'early': '조퇴' }; // 시간 설정 if (workHours) { workHours.value = vacationHours[vacationType] || 8; } // 휴가용 기본값 설정 (휴가 관련 항목 찾아서 자동 선택) if (projectSelect && projectSelect.options.length > 1) { // "휴가", "연차", "관리" 등의 키워드가 포함된 프로젝트 찾기 let vacationProjectFound = false; for (let i = 1; i < projectSelect.options.length; i++) { const optionText = projectSelect.options[i].textContent.toLowerCase(); if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) { projectSelect.selectedIndex = i; vacationProjectFound = true; break; } } if (!vacationProjectFound) { projectSelect.selectedIndex = 1; // 첫 번째 프로젝트 선택 } } if (workTypeSelect && workTypeSelect.options.length > 1) { // "휴가", "연차", "관리" 등의 키워드가 포함된 작업 유형 찾기 let vacationWorkTypeFound = false; for (let i = 1; i < workTypeSelect.options.length; i++) { const optionText = workTypeSelect.options[i].textContent.toLowerCase(); if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) { workTypeSelect.selectedIndex = i; vacationWorkTypeFound = true; break; } } if (!vacationWorkTypeFound) { workTypeSelect.selectedIndex = 1; // 첫 번째 작업 유형 선택 } } if (workStatusSelect && workStatusSelect.options.length > 1) { // "정상", "완료" 등의 키워드가 포함된 상태 찾기 let normalStatusFound = false; for (let i = 1; i < workStatusSelect.options.length; i++) { const optionText = workStatusSelect.options[i].textContent.toLowerCase(); if (optionText.includes('정상') || optionText.includes('완료') || optionText.includes('normal')) { workStatusSelect.selectedIndex = i; normalStatusFound = true; break; } } if (!normalStatusFound) { workStatusSelect.selectedIndex = 1; // 첫 번째 상태 선택 } } // 오류 유형은 선택하지 않음 if (errorTypeSelect) { errorTypeSelect.selectedIndex = 0; } // 작업 설명에 휴가 정보 입력 if (workDescription) { workDescription.value = `${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)`; } // 사용자에게 알림 showToast(`${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)이 설정되었습니다.`, 'success'); } // 탭 전환 함수 function switchTab(tabName) { // 탭 버튼 활성화 상태 변경 document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // 탭 콘텐츠 표시/숨김 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}WorkTab`).classList.add('active'); // 새 작업 탭으로 전환할 때 드롭다운 데이터 로드 if (tabName === 'new') { loadDropdownData(); } } // 전역 함수로 노출 // 드롭다운 로딩 함수들 async function loadDropdownData() { try { console.log('🔄 드롭다운 데이터 로딩 시작...'); // 프로젝트 로드 console.log('📡 프로젝트 로딩 중...'); const projectsRes = await window.apiCall('/projects/active/list'); const projects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []); console.log('📁 로드된 프로젝트:', projects.length, '개'); const projectSelect = document.getElementById('projectSelect'); if (projectSelect) { projectSelect.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.project_id; option.textContent = project.project_name; projectSelect.appendChild(option); }); console.log('✅ 프로젝트 드롭다운 업데이트 완료'); } else { console.error('❌ projectSelect 요소를 찾을 수 없음'); } // 작업 유형 로드 console.log('📡 작업 유형 로딩 중...'); const workTypesRes = await window.apiCall('/daily-work-reports/work-types'); const workTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []); console.log('🔧 로드된 작업 유형:', workTypes.length, '개'); const workTypeSelect = document.getElementById('workTypeSelect'); if (workTypeSelect) { workTypeSelect.innerHTML = ''; workTypes.forEach(workType => { const option = document.createElement('option'); option.value = workType.id; // work_type_id → id option.textContent = workType.name; // work_type_name → name workTypeSelect.appendChild(option); }); console.log('✅ 작업 유형 드롭다운 업데이트 완료'); } else { console.error('❌ workTypeSelect 요소를 찾을 수 없음'); } // 작업 상태 로드 console.log('📡 작업 상태 로딩 중...'); const workStatusRes = await window.apiCall('/daily-work-reports/work-status-types'); const workStatuses = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []); console.log('📊 로드된 작업 상태:', workStatuses.length, '개'); const workStatusSelect = document.getElementById('workStatusSelect'); if (workStatusSelect) { workStatusSelect.innerHTML = ''; workStatuses.forEach(status => { const option = document.createElement('option'); option.value = status.id; // work_status_id → id option.textContent = status.name; // status_name → name workStatusSelect.appendChild(option); }); console.log('✅ 작업 상태 드롭다운 업데이트 완료'); } else { console.error('❌ workStatusSelect 요소를 찾을 수 없음'); } // 오류 유형 로드 console.log('📡 오류 유형 로딩 중...'); const errorTypesRes = await window.apiCall('/daily-work-reports/error-types'); const errorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []); console.log('⚠️ 로드된 오류 유형:', errorTypes.length, '개'); const errorTypeSelect = document.getElementById('errorTypeSelect'); if (errorTypeSelect) { errorTypeSelect.innerHTML = ''; errorTypes.forEach(errorType => { const option = document.createElement('option'); option.value = errorType.id; // error_type_id → id option.textContent = errorType.name; // error_type_name → name errorTypeSelect.appendChild(option); }); console.log('✅ 오류 유형 드롭다운 업데이트 완료'); } else { console.error('❌ errorTypeSelect 요소를 찾을 수 없음'); } console.log('🎉 모든 드롭다운 데이터 로딩 완료!'); } catch (error) { console.error('❌ 드롭다운 데이터 로딩 오류:', error); } } window.openDailyWorkModal = openDailyWorkModal; window.closeDailyWorkModal = closeDailyWorkModal; window.openWorkerModal = openWorkerModal; window.openWorkEntryModal = openWorkEntryModal; window.closeWorkEntryModal = closeWorkEntryModal; window.handleVacation = handleVacation; window.saveWorkEntry = saveWorkEntry; window.switchTab = switchTab; window.editWork = editWork; window.confirmDeleteWork = confirmDeleteWork; window.deleteWork = deleteWork; window.loadDropdownData = loadDropdownData;