import React, { useState, useEffect } from 'react'; import { Calendar, Clock, Users, AlertTriangle, CheckCircle, Edit3, Filter } from 'lucide-react'; const AttendanceValidationPage = () => { const [currentDate, setCurrentDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); const [attendanceData, setAttendanceData] = useState({}); const [selectedDateWorkers, setSelectedDateWorkers] = useState([]); const [filter, setFilter] = useState('all'); // all, needsReview, normal, missing const [monthlyData, setMonthlyData] = useState({ workReports: [], dailyReports: [] }); const [loading, setLoading] = useState(false); const [isAuthorized, setIsAuthorized] = useState(true); // TODO: 실제 권한 체크 // 월이 변경될 때마다 해당 월의 전체 데이터 로드 useEffect(() => { loadMonthlyData(); }, [currentDate]); const loadMonthlyData = async () => { setLoading(true); try { const year = currentDate.getFullYear(); const month = currentDate.getMonth() + 1; console.log(`${year}년 ${month}월 데이터 로딩 중...`); const data = await fetchMonthlyData(year, month); setMonthlyData(data); } catch (error) { console.error('월간 데이터 로딩 실패:', error); // 실패 시 빈 데이터로 설정 setMonthlyData({ workReports: [], dailyReports: [] }); } finally { setLoading(false); } }; // 실제 API 호출 함수들 const fetchWorkReports = async (date) => { try { const response = await fetch(`/api/workreports/date/${date}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` } }); return await response.json(); } catch (error) { console.error('WorkReports API 호출 오류:', error); return []; } }; const fetchDailyWorkReports = async (date) => { try { const response = await fetch(`/api/daily-work-reports/date/${date}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` } }); return await response.json(); } catch (error) { console.error('DailyWorkReports API 호출 오류:', error); return []; } }; // 월간 데이터 가져오기 (캘린더용) const fetchMonthlyData = async (year, month) => { const start = `${year}-${month.toString().padStart(2, '0')}-01`; const end = `${year}-${month.toString().padStart(2, '0')}-31`; try { const [workReports, dailyReports] = await Promise.all([ fetch(`/api/workreports?start=${start}&end=${end}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` } }).then(res => res.json()), fetch(`/api/daily-work-reports/search?start_date=${start}&end_date=${end}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` } }).then(res => res.json()) ]); return { workReports, dailyReports: dailyReports.reports || [] }; } catch (error) { console.error('월간 데이터 가져오기 오류:', error); return { workReports: [], dailyReports: [] }; } }; // 목업 데이터 (개발/테스트용) const mockWorkReports = { '2025-06-16': [ { worker_id: 1, worker_name: '김철수', overtime_hours: 1, status: 'normal' }, { worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'half_day' }, { worker_id: 3, worker_name: '박민수', overtime_hours: 0, status: 'vacation' }, ], '2025-06-17': [ { worker_id: 1, worker_name: '김철수', overtime_hours: 2, status: 'normal' }, { worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'normal' }, { worker_id: 4, worker_name: '정수현', overtime_hours: 0, status: 'early_leave' }, ], '2025-06-19': [ { worker_id: 1, worker_name: '김철수', overtime_hours: 1, status: 'normal' }, { worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'half_day' }, { worker_id: 3, worker_name: '박민수', overtime_hours: 0, status: 'vacation' }, { worker_id: 4, worker_name: '정수현', overtime_hours: 0, status: 'normal' }, ] }; const mockDailyReports = { '2025-06-16': [ { worker_id: 1, worker_name: '김철수', work_hours: 9 }, { worker_id: 2, worker_name: '이영희', work_hours: 4 }, { worker_id: 3, worker_name: '박민수', work_hours: 0 }, ], '2025-06-17': [ { worker_id: 1, worker_name: '김철수', work_hours: 10 }, { worker_id: 2, worker_name: '이영희', work_hours: 8 }, { worker_id: 4, worker_name: '정수현', work_hours: 6 }, ], '2025-06-19': [ { worker_id: 1, worker_name: '김철수', work_hours: 9 }, { worker_id: 2, worker_name: '이영희', work_hours: 4 }, { worker_id: 3, worker_name: '박민수', work_hours: 0 }, // 정수현 데이터 누락 - 미입력 상태 ] }; // 시간 계산 함수 const calculateExpectedHours = (status, overtime_hours = 0) => { const baseHours = { 'normal': 8, 'half_day': 4, 'early_leave': 6, 'quarter_day': 2, 'vacation': 0, 'sick_leave': 0 }; return (baseHours[status] || 8) + (overtime_hours || 0); }; // 날짜별 상태 계산 (월간 데이터 기반) const calculateDateStatus = (dateStr) => { const workReports = monthlyData.workReports.filter(wr => wr.date === dateStr); const dailyReports = monthlyData.dailyReports.filter(dr => dr.report_date === dateStr); if (workReports.length === 0 && dailyReports.length === 0) { return 'no-data'; } if (workReports.length === 0 || dailyReports.length === 0) { return 'missing'; } // 작업자별 시간 집계 const dailyGrouped = dailyReports.reduce((acc, dr) => { if (!acc[dr.worker_id]) { acc[dr.worker_id] = 0; } acc[dr.worker_id] += parseFloat(dr.work_hours || 0); return acc; }, {}); // 불일치 검사 const hasDiscrepancy = workReports.some(wr => { const reportedHours = dailyGrouped[wr.worker_id] || 0; const expectedHours = calculateExpectedHours('normal', wr.overtime_hours || 0); return Math.abs(reportedHours - expectedHours) > 0; }); return hasDiscrepancy ? 'needs-review' : 'normal'; }; // 권한 체크 (실제 구현 시) const checkPermission = () => { // TODO: 실제 권한 체크 로직 const userRole = localStorage.getItem('userRole') || 'user'; return userRole === 'admin' || userRole === 'manager'; }; // 권한 없음 UI if (!isAuthorized) { return (
🔒

# 접근 권한이 없습니다

이 페이지는 관리자(Admin) 이상만 접근 가능합니다.

); } // 로딩 중 UI if (loading) { return (
데이터를 불러오는 중...
); } // 캘린더 생성 const generateCalendar = () => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDate = new Date(firstDay); startDate.setDate(startDate.getDate() - firstDay.getDay()); const calendar = []; const current = new Date(startDate); for (let week = 0; week < 6; week++) { const weekDays = []; for (let day = 0; day < 7; day++) { const dateStr = current.toISOString().split('T')[0]; const isCurrentMonth = current.getMonth() === month; const status = isCurrentMonth ? calculateDateStatus(dateStr) : 'no-data'; weekDays.push({ date: new Date(current), dateStr, isCurrentMonth, status }); current.setDate(current.getDate() + 1); } calendar.push(weekDays); } return calendar; }; // 실제 API를 사용한 작업자 데이터 조합 const getWorkersForDate = async (dateStr) => { try { // 실제 API 호출 const [workReports, dailyReports] = await Promise.all([ fetchWorkReports(dateStr), fetchDailyWorkReports(dateStr) ]); console.log('API 응답:', { workReports, dailyReports }); const workerMap = new Map(); // WorkReports 데이터 추가 (생산지원팀 입력) workReports.forEach(wr => { workerMap.set(wr.worker_id, { worker_id: wr.worker_id, worker_name: wr.worker_name, overtime_hours: wr.overtime_hours || 0, status: 'normal', // 실제 테이블 구조에 맞게 수정 필요 expected_hours: calculateExpectedHours('normal', wr.overtime_hours), reported_hours: null, hasWorkReport: true, hasDailyReport: false }); }); // DailyReports 데이터 추가 (그룹장 입력) - 작업자별 총 시간 집계 const dailyGrouped = dailyReports.reduce((acc, dr) => { if (!acc[dr.worker_id]) { acc[dr.worker_id] = { worker_id: dr.worker_id, worker_name: dr.worker_name, total_work_hours: 0 }; } acc[dr.worker_id].total_work_hours += parseFloat(dr.work_hours || 0); return acc; }, {}); Object.values(dailyGrouped).forEach(dr => { if (workerMap.has(dr.worker_id)) { const worker = workerMap.get(dr.worker_id); worker.reported_hours = dr.total_work_hours; worker.hasDailyReport = true; } else { workerMap.set(dr.worker_id, { worker_id: dr.worker_id, worker_name: dr.worker_name, overtime_hours: 0, status: 'normal', expected_hours: 8, reported_hours: dr.total_work_hours, hasWorkReport: false, hasDailyReport: true }); } }); return Array.from(workerMap.values()).map(worker => ({ ...worker, difference: worker.reported_hours !== null ? worker.reported_hours - worker.expected_hours : -worker.expected_hours, validationStatus: getValidationStatus(worker) })); } catch (error) { console.error('데이터 조합 오류:', error); // 오류 시 목업 데이터 사용 return getWorkersForDateMock(dateStr); } }; // 목업 데이터용 함수 (개발/테스트) const getWorkersForDateMock = (dateStr) => { const workReports = mockWorkReports[dateStr] || []; const dailyReports = mockDailyReports[dateStr] || []; const workerMap = new Map(); // WorkReports 데이터 추가 workReports.forEach(wr => { workerMap.set(wr.worker_id, { worker_id: wr.worker_id, worker_name: wr.worker_name, overtime_hours: wr.overtime_hours, status: wr.status, expected_hours: calculateExpectedHours(wr.status, wr.overtime_hours), reported_hours: null, hasWorkReport: true, hasDailyReport: false }); }); // DailyReports 데이터 추가 dailyReports.forEach(dr => { if (workerMap.has(dr.worker_id)) { const worker = workerMap.get(dr.worker_id); worker.reported_hours = dr.work_hours; worker.hasDailyReport = true; } else { workerMap.set(dr.worker_id, { worker_id: dr.worker_id, worker_name: dr.worker_name, overtime_hours: 0, status: 'normal', expected_hours: 8, reported_hours: dr.work_hours, hasWorkReport: false, hasDailyReport: true }); } }); return Array.from(workerMap.values()).map(worker => ({ ...worker, difference: worker.reported_hours !== null ? worker.reported_hours - worker.expected_hours : -worker.expected_hours, validationStatus: getValidationStatus(worker) })); }; const getValidationStatus = (worker) => { if (!worker.hasWorkReport || !worker.hasDailyReport) return 'missing'; if (Math.abs(worker.difference) > 0) return 'needs-review'; return 'normal'; }; // 작업자 수정 핸들러 const handleEditWorker = (worker) => { // TODO: 수정 모달 또는 인라인 편집 구현 const newHours = prompt( `${worker.worker_name}의 근무시간을 수정하세요.\n현재: ${worker.reported_hours || 0}시간`, worker.reported_hours || 0 ); if (newHours !== null && !isNaN(newHours)) { updateWorkerHours(worker.worker_id, parseFloat(newHours)); } }; // 작업자 시간 업데이트 (실제 API 호출) const updateWorkerHours = async (workerId, newHours) => { try { // TODO: 실제 수정 API 호출 console.log(`작업자 ${workerId}의 시간을 ${newHours}시간으로 수정`); // 임시: 로컬 상태 업데이트 setSelectedDateWorkers(prev => prev.map(worker => worker.worker_id === workerId ? { ...worker, reported_hours: newHours, difference: newHours - worker.expected_hours, validationStatus: Math.abs(newHours - worker.expected_hours) > 0 ? 'needs-review' : 'normal' } : worker ) ); alert('수정이 완료되었습니다.'); } catch (error) { console.error('수정 실패:', error); alert('수정 중 오류가 발생했습니다.'); } }; // 날짜 클릭 핸들러 (실제 API 호출) const handleDateClick = async (dateInfo) => { if (!dateInfo.isCurrentMonth) return; setSelectedDate(dateInfo.dateStr); try { // 로딩 상태 표시 setSelectedDateWorkers([]); // 실제 API에서 데이터 가져오기 const workers = await getWorkersForDate(dateInfo.dateStr); setSelectedDateWorkers(workers); } catch (error) { console.error('날짜별 데이터 로딩 오류:', error); // 오류 시 목업 데이터 사용 const workers = getWorkersForDateMock(dateInfo.dateStr); setSelectedDateWorkers(workers); } }; // 필터링된 작업자 목록 const filteredWorkers = selectedDateWorkers.filter(worker => { if (filter === 'all') return true; if (filter === 'needsReview') return worker.validationStatus === 'needs-review'; if (filter === 'normal') return worker.validationStatus === 'normal'; if (filter === 'missing') return worker.validationStatus === 'missing'; return true; }); // 상태별 아이콘 및 색상 const getStatusIcon = (status) => { switch (status) { case 'normal': return ; case 'needs-review': return ; case 'missing': return ; default: return null; } }; const getStatusColor = (status) => { switch (status) { case 'normal': return 'bg-green-100 text-green-800'; case 'needs-review': return 'bg-yellow-100 text-yellow-800'; case 'missing': return 'bg-red-100 text-red-800'; default: return 'bg-gray-100 text-gray-800'; } }; const calendar = generateCalendar(); const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; const dayNames = ['일', '월', '화', '수', '목', '금', '토']; return (
{/* 헤더 */}

# 근태 검증 관리

Admin 전용 페이지
{/* 캘린더 섹션 */}
{/* 캘린더 헤더 */}

## {currentDate.getFullYear()}년 {monthNames[currentDate.getMonth()]}

{/* 월간 요약 정보 */}
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'normal').length}
정상
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'needs-review').length}
검토필요
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'missing').length}
미입력
{/* 요일 헤더 */}
{dayNames.map(day => (
{day}
))}
{/* 캘린더 본체 */}
{calendar.flat().map((dateInfo, index) => (
))}
{/* 범례 */}
정상
검토필요
미입력
{/* 작업자 리스트 섹션 */}
{selectedDate ? (

### 📅 {selectedDate}

{filteredWorkers.map(worker => (
{worker.worker_name} {getStatusIcon(worker.validationStatus)}
그룹장 입력: {worker.reported_hours !== null ? `${worker.reported_hours}시간` : '미입력'}
시스템 계산: {worker.expected_hours}시간
{worker.difference !== 0 && (
차이: 0 ? 'text-red-600' : 'text-blue-600'}`}> {worker.difference > 0 ? '+' : ''}{worker.difference}시간
)}
{worker.validationStatus === 'needs-review' && (
)}
))}
{filteredWorkers.length === 0 && (
해당 조건의 작업자가 없습니다.
)}
) : (

날짜를 선택하면

작업자 검증 내역을 확인할 수 있습니다.

)}
); }; export default AttendanceValidationPage;