// js/work-report-review.js - 휴가 연동 완전판 import { API, getAuthHeaders } from '/js/api-config.js'; class WorkReportReviewManager { constructor() { this.workers = []; this.projects = []; this.workTypes = []; this.workStatusTypes = []; this.errorTypes = []; this.reports = []; this.filteredReports = []; this.attendanceData = []; // 휴가 정보 this.reviewedReports = new Set(); // 검토완료된 보고서 ID this.selectedReport = null; this.init(); } async init() { try { // 필수 DOM 요소 확인 const requiredElements = ['reportsTableBody', 'editPanelContent', 'totalReports']; const missingElements = requiredElements.filter(id => !document.getElementById(id)); if (missingElements.length > 0) { console.error('필수 DOM 요소가 없습니다:', missingElements); return; } this.showLoading(true); await this.loadData(); this.setupEventListeners(); this.setDefaultDates(); await this.loadReports(); this.showLoading(false); } catch (error) { this.showMessage('데이터 로드 중 오류가 발생했습니다.', 'error'); console.error('초기화 오류:', error); this.showLoading(false); } } async loadData() { try { // 기본 데이터 로딩 const [workersRes, projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([ fetch(`${API}/workers`, { headers: getAuthHeaders() }), fetch(`${API}/projects/active/list`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/work-types`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/work-status-types`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/error-types`, { headers: getAuthHeaders() }) ]); if (!workersRes.ok || !projectsRes.ok) { throw new Error(`API 응답 오류: Workers(${workersRes.status}), Projects(${projectsRes.status})`); } const allWorkers = await workersRes.json(); // 활성화된 작업자만 필터링 this.workers = allWorkers.filter(worker => { return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true; }); this.projects = await projectsRes.json(); // 새로운 API들 로드 (실패해도 기본값 사용) try { this.workTypes = workTypesRes.ok ? await workTypesRes.json() : [ {id: 1, name: 'Base'}, {id: 2, name: 'Vessel'}, {id: 3, name: 'Piping'} ]; this.workStatusTypes = workStatusRes.ok ? await workStatusRes.json() : [ {id: 1, name: '정규'}, {id: 2, name: '에러'} ]; this.errorTypes = errorTypesRes.ok ? await errorTypesRes.json() : [ {id: 1, name: '설계미스'}, {id: 2, name: '외주작업 불량'}, {id: 3, name: '입고지연'}, {id: 4, name: '작업 불량'} ]; } catch (error) { console.log('⚠️ 일부 API 사용 불가, 기본값 사용'); } // 휴가 정보 로드 await this.loadAttendanceData(); this.populateFilters(); console.log('기본 데이터 로드 완료'); } catch (error) { console.error('기본 데이터 로드 오류:', error); throw error; } } async loadAttendanceData() { try { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (!startDate || !endDate) return; // 휴가 정보 API 호출 (예상 엔드포인트) const response = await fetch(`${API}/attendance?start_date=${startDate}&end_date=${endDate}`, { headers: getAuthHeaders() }); if (response.ok) { this.attendanceData = await response.json(); console.log('휴가 정보 로드 완료:', this.attendanceData.length); } else if (response.status === 404) { console.log('⚠️ 휴가 API 없음, 더미 데이터 생성'); this.attendanceData = this.generateDummyAttendance(); } else { throw new Error(`휴가 정보 로드 실패: ${response.status}`); } } catch (error) { console.log('⚠️ 휴가 정보 로드 오류, 더미 데이터 사용:', error.message); this.attendanceData = this.generateDummyAttendance(); } } generateDummyAttendance() { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (!startDate || !endDate) return []; const dummyAttendance = []; const start = new Date(startDate); const end = new Date(endDate); // 날짜별로 작업자들의 출근 상태 생성 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { const dateStr = d.toISOString().split('T')[0]; this.workers.forEach(worker => { const rand = Math.random(); let attendanceType = 'NORMAL'; // 기본값 // 5% 확률로 휴가 if (rand < 0.05) { const types = ['HALF_DAY', 'HALF_HALF_DAY', 'EARLY_LEAVE']; attendanceType = types[Math.floor(Math.random() * types.length)]; } dummyAttendance.push({ id: `att_${worker.worker_id}_${dateStr}`, worker_id: worker.worker_id, worker_name: worker.worker_name, date: dateStr, attendance_type: attendanceType, expected_hours: this.getExpectedHours(attendanceType) }); }); } return dummyAttendance; } getExpectedHours(attendanceType) { switch (attendanceType) { case 'HALF_DAY': return 4; // 반차 case 'HALF_HALF_DAY': return 6; // 반반차 case 'EARLY_LEAVE': return 2; // 조퇴 case 'NORMAL': default: return 8; // 일반근무 } } getAttendanceTypeName(attendanceType) { switch (attendanceType) { case 'HALF_DAY': return '반차'; case 'HALF_HALF_DAY': return '반반차'; case 'EARLY_LEAVE': return '조퇴'; case 'NORMAL': default: return '정상근무'; } } async loadReports() { try { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const workerId = document.getElementById('workerFilter').value; const projectId = document.getElementById('projectFilter').value; // 필수 조건 체크 if (!startDate || !endDate) { this.showMessage('시작 날짜와 종료 날짜는 필수입니다.', 'error'); return; } // 휴가 정보 먼저 로드 await this.loadAttendanceData(); let url = `${API}/daily-work-reports/search?`; const params = new URLSearchParams(); if (startDate) params.append('start_date', startDate); if (endDate) params.append('end_date', endDate); if (workerId) params.append('worker_id', workerId); if (projectId) params.append('project_id', projectId); // 페이지네이션 (일단 많이 가져오기) params.append('page', '1'); params.append('limit', '1000'); url += params.toString(); const response = await fetch(url, { headers: getAuthHeaders() }); if (response.status === 404 || response.status === 500) { // API가 아직 준비되지 않은 경우 더미 데이터 사용 this.reports = this.generateDummyData(); console.log('⚠️ API 응답 오류, 더미 데이터 사용:', response.status); if (response.status === 404) { this.showMessage('⚠️ 검토 API가 준비되지 않아 더미 데이터를 표시합니다.', 'warning'); } else { this.showMessage('⚠️ 서버 오류로 더미 데이터를 표시합니다.', 'warning'); } } else if (response.ok) { const result = await response.json(); this.reports = result.reports || []; console.log('검색 결과:', result.summary); } else { throw new Error(`보고서 로드 실패: ${response.status}`); } // 근무시간 검증 추가 this.validateWorkHours(); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); } catch (error) { console.log('⚠️ 네트워크 오류로 더미 데이터 사용:', error.message); // 더미 데이터로 대체 this.reports = this.generateDummyData(); this.validateWorkHours(); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); // 사용자에게는 부드러운 메시지 표시 this.showMessage('💡 데모 데이터로 화면을 구성했습니다.', 'info'); } } validateWorkHours() { // 각 보고서에 대해 근무시간 검증 this.reports.forEach(report => { const attendance = this.attendanceData.find(att => att.worker_id === report.worker_id && att.date === report.report_date ); if (attendance) { const expectedHours = attendance.expected_hours; const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); report.expected_hours = expectedHours; report.actual_hours = actualHours; report.attendance_type = attendance.attendance_type; report.hours_status = this.getHoursStatus(actualHours, expectedHours); report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); } else { // 휴가 정보가 없으면 기본 8시간으로 가정 const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); report.expected_hours = 8; report.actual_hours = actualHours; report.attendance_type = 'NORMAL'; report.hours_status = this.getHoursStatus(actualHours, 8); report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); } }); } getWorkerDailyHours(workerId, date) { // 해당 작업자의 특정 날짜 총 작업시간 계산 return this.reports .filter(r => r.worker_id === workerId && r.report_date === date) .reduce((sum, r) => sum + (r.work_hours || 0), 0); } getHoursStatus(actualHours, expectedHours) { const tolerance = 0.5; // 30분 허용 오차 if (Math.abs(actualHours - expectedHours) <= tolerance) { return 'NORMAL'; // 정상 } else if (actualHours < expectedHours) { return 'UNDER'; // 부족 } else { return 'OVER'; // 초과 } } generateDummyData() { const today = new Date(); const dummyData = []; // 더미 데이터 생성 (최근 7일) for (let i = 0; i < 7; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; this.workers.forEach(worker => { // 80% 확률로 보고서 생성 if (Math.random() > 0.2) { const numEntries = Math.floor(Math.random() * 3) + 1; // 1-3개 작업 for (let j = 0; j < numEntries; j++) { const workStatus = Math.random() > 0.9 ? 2 : 1; // 10% 확률로 에러 const workHours = Math.random() > 0.95 ? (Math.random() * 4 + 10) : // 5% 확률로 이상 시간 (10-14시간) (Math.random() * 6 + 2); // 정상 시간 (2-8시간) dummyData.push({ id: 1000000 + i * 100 + worker.worker_id * 10 + j, // 고유 정수 ID report_date: dateStr, worker_id: worker.worker_id, worker_name: worker.worker_name, project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1, project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown', work_type_id: Math.floor(Math.random() * 3) + 1, work_type_name: this.workTypes[Math.floor(Math.random() * 3)]?.name || 'Base', work_status_id: workStatus, work_status_name: workStatus === 2 ? '에러' : '정규', error_type_id: workStatus === 2 ? Math.floor(Math.random() * 4) + 1 : null, error_type_name: workStatus === 2 ? this.errorTypes[Math.floor(Math.random() * 4)]?.name : null, work_hours: Math.round(workHours * 2) / 2, // 0.5시간 단위 created_at: `${dateStr}T${String(Math.floor(Math.random() * 24)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:00` }); } } }); } return dummyData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); } populateFilters() { // 작업자 필터 const workerSelect = document.getElementById('workerFilter'); if (workerSelect) { workerSelect.innerHTML = ''; this.workers.forEach(worker => { workerSelect.innerHTML += ``; }); } // 프로젝트 필터 const projectSelect = document.getElementById('projectFilter'); if (projectSelect) { projectSelect.innerHTML = ''; this.projects.forEach(project => { projectSelect.innerHTML += ``; }); } } setDefaultDates() { const today = new Date(); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); const startDateInput = document.getElementById('startDate'); const endDateInput = document.getElementById('endDate'); if (startDateInput) startDateInput.value = weekAgo.toISOString().split('T')[0]; if (endDateInput) endDateInput.value = today.toISOString().split('T')[0]; } updateDashboard() { // 날짜별, 작업자별로 그룹화하여 계산 const groupedReports = this.groupReportsByWorkerAndDate(); const groups = Object.values(groupedReports); const total = groups.length; const errors = groups.filter(group => group.some(r => r.work_status_id === 2)).length; const warnings = groups.filter(group => { const firstReport = group[0]; return firstReport.hours_status === 'UNDER' || firstReport.hours_status === 'OVER'; }).length; const reviewed = groups.filter(group => group[0].is_reviewed).length; document.getElementById('totalReports').textContent = total; document.getElementById('errorReports').textContent = errors; document.getElementById('warningReports').textContent = warnings; document.getElementById('missingReports').textContent = total - reviewed; // 미검토 건수 } updateAlertsSection() { const alertsList = document.getElementById('alertsList'); if (!alertsList) return; const alerts = []; const groupedReports = this.groupReportsByWorkerAndDate(); // 각 그룹별로 알림 체크 Object.values(groupedReports).forEach(group => { const firstReport = group[0]; const hasError = group.some(r => r.work_status_id === 2); const workerName = firstReport.worker_name; const date = firstReport.report_date; // 에러 발생 건 if (hasError) { const errorReports = group.filter(r => r.work_status_id === 2); alerts.push({ type: 'error', text: `${workerName} (${date}) - 에러 ${errorReports.length}건`, time: this.formatDate(date), priority: 1 }); } // 시간 불일치 if (firstReport.hours_status === 'UNDER' || firstReport.hours_status === 'OVER') { const statusText = firstReport.hours_status === 'UNDER' ? '부족' : '초과'; alerts.push({ type: 'warning', text: `${workerName} (${date}) - 시간 ${statusText} (${firstReport.actual_hours}h/${firstReport.expected_hours}h)`, time: this.formatDate(date), priority: 2 }); } // 검토 대기 (정상이지만 아직 검토되지 않음) if (!firstReport.is_reviewed && firstReport.hours_status === 'NORMAL' && !hasError) { alerts.push({ type: 'pending', text: `${workerName} (${date}) - 검토 대기`, time: this.formatDate(date), priority: 3 }); } }); // 우선순위별 정렬 alerts.sort((a, b) => a.priority - b.priority); // 알림 표시 if (alerts.length === 0) { alertsList.innerHTML = `
✅ 모든 항목이 정상이거나 검토 완료되었습니다
`; } else { alertsList.innerHTML = alerts.map(alert => `
${this.getAlertTypeLabel(alert.type)} ${alert.text}
${alert.time}
`).join(''); } } getAlertTypeLabel(type) { switch (type) { case 'error': return '에러'; case 'warning': return '주의'; case 'pending': return '대기'; case 'missing': return '미입력'; default: return '알림'; } } updateTable() { const tbody = document.getElementById('reportsTableBody'); if (!tbody) return; if (this.filteredReports.length === 0) { tbody.innerHTML = ` 조회된 데이터가 없습니다 `; return; } // 날짜별, 작업자별로 그룹화 const groupedReports = this.groupReportsByWorkerAndDate(); tbody.innerHTML = Object.entries(groupedReports).map(([key, group]) => { const [workerId, date] = key.split('_'); const firstReport = group[0]; const totalHours = group.reduce((sum, r) => sum + (r.work_hours || 0), 0); const hasError = group.some(r => r.work_status_id === 2); const rowClass = this.getGroupRowClass(firstReport, hasError); return ` ${this.formatDate(firstReport.report_date)} ${firstReport.worker_name} ${this.getAttendanceTypeName(firstReport.attendance_type)} ${firstReport.expected_hours}h ${totalHours}h ${this.getHoursStatusName(firstReport.hours_status)} ${group.map(r => r.project_name).join(', ')} ${group.map(r => r.work_type_name).join(', ')} ${hasError ? '에러 포함' : '정상'} ${firstReport.is_reviewed ? '검토완료' : '검토대기' } ${!firstReport.is_reviewed && firstReport.hours_status === 'NORMAL' && !hasError ? `` : '-' } `; }).join(''); } groupReportsByWorkerAndDate() { const grouped = {}; this.filteredReports.forEach(report => { const key = `${report.worker_id}_${report.report_date}`; if (!grouped[key]) { grouped[key] = []; } grouped[key].push(report); }); return grouped; } getGroupRowClass(report, hasError) { if (report.is_reviewed) return 'row-reviewed'; if (hasError || report.work_status_id === 2) return 'row-error'; if (report.hours_status === 'UNDER' || report.hours_status === 'OVER') return 'row-warning'; return 'row-normal'; } getHoursStatusName(status) { switch (status) { case 'NORMAL': return '정상'; case 'UNDER': return '부족'; case 'OVER': return '초과'; default: return '확인필요'; } } selectWorkerDate(workerId, date) { // 해당 작업자의 특정 날짜 보고서들을 선택 const reports = this.filteredReports.filter(r => r.worker_id == workerId && r.report_date === date ); if (reports.length > 0) { this.selectedReport = reports[0]; // 첫 번째 보고서를 대표로 선택 this.selectedReport.allReports = reports; // 전체 보고서 목록 추가 this.updateEditPanel(); // 테이블에서 선택 표시 document.querySelectorAll('.data-table tr.selected').forEach(tr => { tr.classList.remove('selected'); }); // 해당 행 선택 표시 const rows = document.querySelectorAll('.data-table tr'); rows.forEach(row => { if (row.onclick && row.onclick.toString().includes(`'${workerId}', '${date}'`)) { row.classList.add('selected'); } }); } } async markAsReviewed(workerId, date) { try { const key = `${workerId}_${date}`; // API 호출 (준비되면) // const response = await fetch(`${API}/daily-work-reports/review`, { // method: 'POST', // headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, // body: JSON.stringify({ worker_id: workerId, report_date: date, reviewed: true }) // }); // 현재는 로컬 상태만 업데이트 this.reviewedReports.add(key); // 해당 보고서들의 검토 상태 업데이트 this.reports.forEach(report => { if (report.worker_id == workerId && report.report_date === date) { report.is_reviewed = true; } }); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); this.showMessage(`✅ ${this.getWorkerName(workerId)}의 ${date} 보고서가 검토완료 처리되었습니다.`, 'success'); } catch (error) { this.showMessage('❌ 검토완료 처리 중 오류가 발생했습니다.', 'error'); console.error('검토완료 오류:', error); } } getWorkerName(workerId) { const worker = this.workers.find(w => w.worker_id == workerId); return worker ? worker.worker_name : `작업자${workerId}`; } updateEditPanel() { const panelContent = document.getElementById('editPanelContent'); if (!panelContent || !this.selectedReport) return; const report = this.selectedReport; const allReports = report.allReports || [report]; panelContent.innerHTML = `

${report.worker_name} - ${this.formatDate(report.report_date)}

출근형태:
${this.getAttendanceTypeName(report.attendance_type)}
시간상태:
${this.getHoursStatusName(report.hours_status)}
기대시간: ${report.expected_hours}시간
실제시간: ${report.actual_hours}시간
작업 상세 (${allReports.length}건)
${allReports.map((r, index) => `
프로젝트: ${r.project_name}
작업유형: ${r.work_type_name}
작업시간: ${r.work_hours}시간
상태: ${r.work_status_name}
${r.error_type_name ? `
에러유형: ${r.error_type_name}
` : ''}
`).join('')}
${!report.is_reviewed && report.hours_status === 'NORMAL' && !allReports.some(r => r.work_status_id === 2) ? `` : '' }
`; } editSingleReport(reportId) { // 개별 보고서 수정을 위해 기존 로직 사용 this.selectedReport = this.filteredReports.find(r => r.id == reportId); if (this.selectedReport) { // 기존 수정 폼 표시 this.showEditForm(); } } showEditForm() { const panelContent = document.getElementById('editPanelContent'); if (!panelContent || !this.selectedReport) return; const report = this.selectedReport; panelContent.innerHTML = `
`; // 업무 상태 변경 시 에러 유형 표시/숨김 const workStatusSelect = document.getElementById('editWorkStatus'); const errorTypeGroup = document.getElementById('editErrorTypeGroup'); if (workStatusSelect && errorTypeGroup) { workStatusSelect.addEventListener('change', (e) => { if (e.target.value === '2') { errorTypeGroup.style.display = 'block'; } else { errorTypeGroup.style.display = 'none'; document.getElementById('editErrorType').value = ''; } }); } } async saveReport() { if (!this.selectedReport) return; try { this.showLoading(true); const formData = { report_date: document.getElementById('editReportDate').value, project_id: parseInt(document.getElementById('editProject').value), work_type_id: parseInt(document.getElementById('editWorkType').value), work_status_id: parseInt(document.getElementById('editWorkStatus').value), error_type_id: document.getElementById('editErrorType').value ? parseInt(document.getElementById('editErrorType').value) : null, work_hours: parseFloat(document.getElementById('editWorkHours').value) }; const response = await fetch(`${API}/daily-work-reports/${this.selectedReport.id}`, { method: 'PUT', headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); if (response.status === 404 || response.status === 500) { if (response.status === 404) { this.showMessage('⚠️ 수정 API가 아직 준비되지 않았습니다.', 'warning'); } else { this.showMessage('⚠️ 서버 오류가 발생했습니다. 더미 데이터를 수정합니다.', 'warning'); } // 더미 데이터 업데이트 this.updateDummyData(formData); } else if (response.ok) { this.showMessage('✅ 성공적으로 수정되었습니다.', 'success'); await this.loadReports(); } else { const result = await response.json(); throw new Error(result.error || '수정 중 오류가 발생했습니다.'); } this.clearSelection(); } catch (error) { this.showMessage(`❌ ${error.message}`, 'error'); console.error('수정 오류:', error); } finally { this.showLoading(false); } } updateDummyData(formData) { // 더미 데이터에서 수정 const reportIndex = this.reports.findIndex(r => r.id === this.selectedReport.id); if (reportIndex !== -1) { this.reports[reportIndex] = { ...this.reports[reportIndex], ...formData, project_name: this.projects.find(p => p.project_id === formData.project_id)?.project_name, work_type_name: this.workTypes.find(wt => wt.id === formData.work_type_id)?.name, work_status_name: this.workStatusTypes.find(ws => ws.id === formData.work_status_id)?.name, error_type_name: formData.error_type_id ? this.errorTypes.find(et => et.id === formData.error_type_id)?.name : null }; // 시간 검증 다시 실행 this.validateWorkHours(); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); } } async deleteReport() { if (!this.selectedReport) return; if (!confirm('정말로 이 보고서를 삭제하시겠습니까?')) return; try { this.showLoading(true); const response = await fetch(`${API}/daily-work-reports/${this.selectedReport.id}`, { method: 'DELETE', headers: getAuthHeaders() }); if (response.status === 404 || response.status === 500) { if (response.status === 404) { this.showMessage('⚠️ 삭제 API가 아직 준비되지 않았습니다.', 'warning'); } else { this.showMessage('⚠️ 서버 오류가 발생했습니다. 더미 데이터를 삭제합니다.', 'warning'); } // 더미 데이터에서 삭제 this.reports = this.reports.filter(r => r.id !== this.selectedReport.id); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); } else if (response.ok) { this.showMessage('✅ 성공적으로 삭제되었습니다.', 'success'); await this.loadReports(); } else { const result = await response.json(); throw new Error(result.error || '삭제 중 오류가 발생했습니다.'); } this.clearSelection(); } catch (error) { this.showMessage(`❌ ${error.message}`, 'error'); console.error('삭제 오류:', error); } finally { this.showLoading(false); } } clearSelection() { this.selectedReport = null; // 테이블 선택 해제 document.querySelectorAll('.data-table tr.selected').forEach(tr => { tr.classList.remove('selected'); }); // 패널 초기화 const panelContent = document.getElementById('editPanelContent'); if (panelContent) { panelContent.innerHTML = `
📝
수정할 항목을 선택해주세요
`; } } setupEventListeners() { // 필터 적용 버튼 const applyFilterBtn = document.getElementById('applyFilter'); if (applyFilterBtn) { applyFilterBtn.addEventListener('click', () => { this.loadReports(); }); } // 새로고침 버튼 const refreshBtn = document.getElementById('refreshBtn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.loadReports(); }); } // 내보내기 버튼 const exportBtn = document.getElementById('exportBtn'); if (exportBtn) { exportBtn.addEventListener('click', () => { this.exportData(); }); } // 엔터키로 필터 적용 ['startDate', 'endDate', 'workerFilter', 'projectFilter'].forEach(id => { const element = document.getElementById(id); if (element) { element.addEventListener('change', () => { this.loadReports(); }); } }); } exportData() { if (this.filteredReports.length === 0) { this.showMessage('내보낼 데이터가 없습니다.', 'warning'); return; } // CSV 생성 const headers = ['날짜', '작업자', '출근형태', '기대시간', '실제시간', '시간상태', '프로젝트', '작업유형', '상태', '검토상태']; // 그룹화된 데이터로 CSV 생성 const groupedReports = this.groupReportsByWorkerAndDate(); const csvContent = [ headers.join(','), ...Object.entries(groupedReports).map(([key, group]) => { const firstReport = group[0]; const totalHours = group.reduce((sum, r) => sum + (r.work_hours || 0), 0); const hasError = group.some(r => r.work_status_id === 2); return [ firstReport.report_date, firstReport.worker_name, this.getAttendanceTypeName(firstReport.attendance_type), firstReport.expected_hours, totalHours, this.getHoursStatusName(firstReport.hours_status), group.map(r => r.project_name).join('; '), group.map(r => r.work_type_name).join('; '), hasError ? '에러 포함' : '정상', firstReport.is_reviewed ? '검토완료' : '검토대기' ].join(','); }) ].join('\n'); // 파일 다운로드 const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `작업보고서_검토_${new Date().toISOString().split('T')[0]}.csv`; link.click(); this.showMessage('✅ 데이터가 내보내졌습니다.', 'success'); } formatDate(dateStr) { return new Date(dateStr).toLocaleDateString('ko-KR'); } formatTime(dateTimeStr) { return new Date(dateTimeStr).toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } showLoading(show) { const overlay = document.getElementById('loadingOverlay'); if (overlay) { overlay.style.display = show ? 'flex' : 'none'; } } showMessage(message, type) { const container = document.getElementById('message-container'); if (container) { container.innerHTML = `
${message}
`; if (type === 'success') { setTimeout(() => { container.innerHTML = ''; }, 5000); } } } } // 전역 인스턴스 생성 let workReportReview; // DOM 준비 후 초기화 document.addEventListener('DOMContentLoaded', () => { workReportReview = new WorkReportReviewManager(); // 전역 접근을 위해 window에 할당 window.workReportReview = workReportReview; });