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('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('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('token')}` }
}).then(res => res.json()),
fetch(`/api/daily-work-reports/search?start_date=${start}&end_date=${end}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('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 (
{/* 헤더 */}
{/* 캘린더 섹션 */}
{/* 캘린더 헤더 */}
{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;