Files
Groupware/Project/frontend/src/pages/Jobreport.tsx
backuppc 890e6edab4 nr 구매 제한 기능 추가
- 트리거를 이용하여 기존 프로그램 사용자도 오류가 발생하도록 함
2025-12-12 11:06:13 +09:00

867 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import {
FileText,
Search,
RefreshCw,
Copy,
Info,
Plus,
Calendar,
AlertTriangle,
X,
} from 'lucide-react';
import { comms } from '@/communication';
import { JobReportItem, JobReportUser } from '@/types';
import { JobreportEditModal, JobreportFormData, initialFormData } from '@/components/jobreport/JobreportEditModal';
import { JobReportDayDialog } from '@/components/jobreport/JobReportDayDialog';
import { JobreportTypeModal } from '@/components/jobreport/JobreportTypeModal';
export function Jobreport() {
const [jobreportList, setJobreportList] = useState<JobReportItem[]>([]);
const [users, setUsers] = useState<JobReportUser[]>([]);
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
// 검색 조건
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [selectedUser, setSelectedUser] = useState('');
const [searchKey, setSearchKey] = useState('');
// 모달 상태
const [showModal, setShowModal] = useState(false);
const [showDayReportModal, setShowDayReportModal] = useState(false);
const [showTypeReportModal, setShowTypeReportModal] = useState(false);
const [editingItem, setEditingItem] = useState<JobReportItem | null>(null);
const [formData, setFormData] = useState<JobreportFormData>(initialFormData);
// 페이징 상태
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
// 권한 상태
const [canViewOT, setCanViewOT] = useState(false);
// 오늘 근무시간 상태
const [todayWork, setTodayWork] = useState({ hrs: 0, ot: 0 });
// 미등록 업무일지 상태
const [unregisteredJobReportCount, setUnregisteredJobReportCount] = useState(0);
const [unregisteredJobReportDays, setUnregisteredJobReportDays] = useState<{ date: string; hrs: number }[]>([]);
const [showUnregisteredModal, setShowUnregisteredModal] = useState(false);
// 날짜 포맷 헬퍼 함수 (로컬 시간 기준)
const formatDateLocal = (date: Date) => {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
// 오늘 근무시간 로드
const loadTodayWork = useCallback(async (userId: string) => {
const todayStr = formatDateLocal(new Date());
try {
const response = await comms.getJobReportList(todayStr, todayStr, userId, '');
if (response.Success && response.Data) {
// 웹소켓 모드에서 응답 혼선 방지를 위해 오늘 날짜 데이터만 필터링
const todayData = response.Data.filter(item => {
const itemDate = item.pdate?.substring(0, 10);
return itemDate === todayStr;
});
const work = todayData.reduce((acc, item) => ({
hrs: acc.hrs + (item.hrs || 0),
ot: acc.ot + (item.ot || 0)
}), { hrs: 0, ot: 0 });
setTodayWork(work);
}
} catch (error) {
console.error('오늘 근무시간 로드 오류:', error);
}
}, []);
// 미등록 업무일지 로드
const loadUnregisteredJobReports = useCallback(async (userId: string) => {
try {
const now = new Date();
const todayStr = formatDateLocal(now);
// 15일 전 날짜 계산
const fifteenDaysAgoDate = new Date(now);
fifteenDaysAgoDate.setDate(now.getDate() - 15);
const fifteenDaysAgoStr = formatDateLocal(fifteenDaysAgoDate);
const response = await comms.getJobReportList(fifteenDaysAgoStr, todayStr, userId, '');
if (response.Success && response.Data) {
const dailyWork: { [key: string]: number } = {};
// 날짜별 시간 합계 계산
response.Data.forEach((item: JobReportItem) => {
if (item.pdate) {
const date = item.pdate.substring(0, 10);
dailyWork[date] = (dailyWork[date] || 0) + (item.hrs || 0);
}
});
const insufficientDays: { date: string; hrs: number }[] = [];
// 어제부터 15일 전까지 확인 (오늘은 제외)
for (let i = 1; i <= 15; i++) {
const d = new Date(now);
d.setDate(now.getDate() - i);
const dStr = formatDateLocal(d);
// 주말(토:6, 일:0) 제외
if (d.getDay() === 0 || d.getDay() === 6) continue;
const hrs = dailyWork[dStr] || 0;
if (hrs < 8) {
insufficientDays.push({ date: dStr, hrs });
}
}
setUnregisteredJobReportCount(insufficientDays.length);
setUnregisteredJobReportDays(insufficientDays);
}
} catch (error) {
console.error('미등록 업무일지 로드 오류:', error);
}
}, []);
// 초기화 완료 플래그
const [initialized, setInitialized] = useState(false);
// 날짜 및 사용자 정보 초기화
useEffect(() => {
const initialize = async () => {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const sd = formatDateLocal(startOfMonth);
const ed = formatDateLocal(endOfMonth);
setStartDate(sd);
setEndDate(ed);
// 현재 로그인 사용자 정보 로드
let userId = '';
try {
const loginStatus = await comms.checkLoginStatus();
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
userId = loginStatus.User.Id;
setSelectedUser(userId);
}
} catch (error) {
console.error('로그인 정보 로드 오류:', error);
}
// 사용자 목록 로드
loadUsers();
// 권한 로드 (본인 조회이므로 canViewOT = true)
try {
const perm = await comms.getJobReportPermission(userId);
if (perm.Success) {
setCanViewOT(perm.CanViewOT);
}
} catch (error) {
console.error('권한 정보 로드 오류:', error);
}
// 초기화 완료 표시
setInitialized(true);
};
initialize();
}, []);
// 초기화 완료 후 조회 실행 (최초 1회만)
useEffect(() => {
if (initialized && startDate && endDate && selectedUser) {
handleSearchAndLoadToday();
}
}, [initialized]); // startDate, endDate, selectedUser 의존성 제거 (날짜 변경 시 자동 조회 방지)
// 검색 + 오늘 근무시간 로드 (순차 실행)
const handleSearchAndLoadToday = async () => {
await handleSearch();
loadTodayWork(selectedUser);
loadUnregisteredJobReports(selectedUser);
};
// 사용자 목록 로드
const loadUsers = async () => {
try {
const result = await comms.getJobReportUsers();
setUsers(result || []);
} catch (error) {
console.error('사용자 목록 로드 오류:', error);
}
};
// 데이터 로드
const loadData = useCallback(async () => {
if (!startDate || !endDate) return;
setLoading(true);
try {
const response = await comms.getJobReportList(startDate, endDate, selectedUser, searchKey);
if (response.Success && response.Data) {
setJobreportList(response.Data);
} else {
setJobreportList([]);
}
} catch (error) {
console.error('업무일지 목록 로드 오류:', error);
alert('데이터를 불러오는 중 오류가 발생했습니다.');
} finally {
setLoading(false);
}
}, [startDate, endDate, selectedUser, searchKey]);
// 검색
const handleSearch = async () => {
if (new Date(startDate) > new Date(endDate)) {
alert('시작일은 종료일보다 늦을 수 없습니다.');
return;
}
// 선택된 담당자에 따라 권한 재확인
try {
const perm = await comms.getJobReportPermission(selectedUser);
if (perm.Success) {
setCanViewOT(perm.CanViewOT);
}
} catch (error) {
console.error('권한 정보 로드 오류:', error);
}
await loadData();
};
// 새 업무일지 추가 모달
const openAddModal = () => {
setEditingItem(null);
setFormData({
...initialFormData,
pdate: formatDateLocal(new Date())
});
setShowModal(true);
};
// 복사하여 새 업무일지 생성 모달
const openCopyModal = async (item: JobReportItem, e: React.MouseEvent) => {
e.stopPropagation(); // 행 클릭 이벤트 방지
try {
const response = await comms.getJobReportDetail(item.idx);
if (response.Success && response.Data) {
const data = response.Data;
setEditingItem(null); // 새로 추가하는 것이므로 null
setFormData({
pdate: formatDateLocal(new Date()), // 오늘 날짜 (로컬 시간 기준)
projectName: data.projectName || '',
pidx: data.pidx ?? null, // pidx도 복사
requestpart: data.requestpart || '',
package: data.package || '',
type: data.type || '',
process: data.process || '',
status: data.status || '진행 완료',
description: data.description || '',
hrs: 0, // 시간 초기화
ot: 0, // OT 초기화
otStart: data.otStart ? data.otStart.substring(11, 16) : '18:00',
otEnd: data.otEnd ? data.otEnd.substring(11, 16) : '20:00',
jobgrp: data.jobgrp || '',
tag: data.tag || '',
});
setShowModal(true);
}
} catch (error) {
console.error('업무일지 조회 오류:', error);
alert('데이터를 불러오는 중 오류가 발생했습니다.');
}
};
// 편집 모달
const openEditModal = async (item: JobReportItem) => {
try {
const response = await comms.getJobReportDetail(item.idx);
if (response.Success && response.Data) {
const data = response.Data;
setEditingItem(data);
setFormData({
pdate: data.pdate ? data.pdate.split('T')[0] : '',
projectName: data.projectName || '',
pidx: data.pidx ?? null,
requestpart: data.requestpart || '',
package: data.package || '',
type: data.type || '',
process: data.process || '',
status: data.status || '진행 완료',
description: data.description || '',
hrs: data.hrs || 0,
ot: data.ot || 0,
otStart: data.otStart ? data.otStart.substring(11, 16) : '18:00',
otEnd: data.otEnd ? data.otEnd.substring(11, 16) : '20:00',
jobgrp: data.jobgrp || '',
tag: data.tag || '',
});
setShowModal(true);
}
} catch (error) {
console.error('업무일지 조회 오류:', error);
alert('데이터를 불러오는 중 오류가 발생했습니다.');
}
};
// 저장
const handleSave = async () => {
if (!formData.pdate) {
alert('날짜를 입력해주세요.');
return;
}
if (!formData.projectName.trim()) {
alert('프로젝트명을 입력해주세요.');
return;
}
setProcessing(true);
try {
let response;
if (editingItem) {
const itemIdx = editingItem.idx ?? (editingItem as unknown as Record<string, unknown>)['Idx'] as number;
if (!itemIdx) {
alert('수정할 항목의 ID를 찾을 수 없습니다.');
setProcessing(false);
return;
}
response = await comms.editJobReport(
itemIdx,
formData.pdate || '',
formData.projectName || '',
formData.pidx,
formData.requestpart || '',
formData.package || '',
formData.type || '',
formData.process || '',
formData.status || '진행 완료',
formData.description || '',
formData.hrs || 0,
formData.ot || 0,
formData.jobgrp || '',
formData.tag || '',
formData.otStart || '18:00',
formData.otEnd || '20:00'
);
} else {
response = await comms.addJobReport(
formData.pdate || '',
formData.projectName || '',
formData.pidx,
formData.requestpart || '',
formData.package || '',
formData.type || '',
formData.process || '',
formData.status || '진행 완료',
formData.description || '',
formData.hrs || 0,
formData.ot || 0,
formData.jobgrp || '',
formData.tag || '',
formData.otStart || '18:00',
formData.otEnd || '20:00'
);
}
if (response.Success) {
setShowModal(false);
loadData();
loadTodayWork(selectedUser);
loadUnregisteredJobReports(selectedUser);
} else {
alert(response.Message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('저장 오류:', error);
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setProcessing(false);
}
};
// 삭제
const handleDelete = async (id: number) => {
if (!confirm('정말로 이 업무일지를 삭제하시겠습니까?')) return;
setProcessing(true);
try {
const response = await comms.deleteJobReport(id);
if (response.Success) {
alert('삭제되었습니다.');
loadData();
loadTodayWork(selectedUser);
loadUnregisteredJobReports(selectedUser);
} else {
alert(response.Message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 오류:', error);
alert('서버 연결에 실패했습니다.');
} finally {
setProcessing(false);
}
};
// 날짜 포맷 (YY.MM.DD)
const formatDate = (dateStr: string | null) => {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
const yy = String(date.getFullYear()).slice(-2);
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yy}.${mm}.${dd}`;
} catch {
return dateStr;
}
};
// 페이징 계산
const totalPages = Math.ceil(jobreportList.length / pageSize);
const paginatedList = jobreportList.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
);
// 검색 시 페이지 초기화
const handleSearchWithReset = () => {
setCurrentPage(1);
handleSearch();
};
// 빠른 날짜 선택 함수들
const setToday = () => {
const today = new Date();
setStartDate(formatDateLocal(today));
};
const setThisMonth = () => {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
setStartDate(formatDateLocal(startOfMonth));
setEndDate(formatDateLocal(endOfMonth));
};
const setYesterday = () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
setStartDate(formatDateLocal(yesterday));
};
const setLastMonth = () => {
const now = new Date();
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
setStartDate(formatDateLocal(lastMonthStart));
setEndDate(formatDateLocal(lastMonthEnd));
};
return (
<div className="space-y-6 animate-fade-in">
{/* 검색 필터 */}
<div className="glass-effect rounded-2xl p-6">
<div className="flex gap-6">
{/* 좌측: 필터 영역 */}
<div className="flex-1">
<div className="flex items-start gap-3">
{/* 빠른 날짜 선택 버튼 - 2x2 그리드 */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={setToday}
className="h-10 bg-white/10 hover:bg-white/20 text-white text-xs px-2 rounded-lg transition-colors whitespace-nowrap"
title="오늘 날짜로 설정"
>
</button>
<button
onClick={setYesterday}
className="h-10 bg-white/10 hover:bg-white/20 text-white text-xs px-2 rounded-lg transition-colors whitespace-nowrap"
title="어제 날짜로 설정"
>
</button>
<button
onClick={setThisMonth}
className="h-10 bg-white/10 hover:bg-white/20 text-white text-xs px-2 rounded-lg transition-colors whitespace-nowrap"
title="이번 달 1일부터 말일까지"
>
</button>
<button
onClick={setLastMonth}
className="h-10 bg-white/10 hover:bg-white/20 text-white text-xs px-2 rounded-lg transition-colors whitespace-nowrap"
title="저번달 1일부터 말일까지"
>
</button>
</div>
{/* 필터 입력 영역: 2행 2열 */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
{/* 1행: 시작일, 담당자 */}
<div className="flex items-center gap-2">
<label className="text-white/70 text-sm font-medium whitespace-nowrap w-12"></label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-white/70 text-sm font-medium whitespace-nowrap w-12"></label>
<select
value={selectedUser}
onChange={(e) => setSelectedUser(e.target.value)}
className="w-44 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
>
<option value="" className="bg-gray-800"></option>
{users.map((user) => (
<option key={user.id} value={user.id} className="bg-gray-800">
{user.name}({user.id})
</option>
))}
</select>
</div>
{/* 2행: 종료일, 검색어 */}
<div className="flex items-center gap-2">
<label className="text-white/70 text-sm font-medium whitespace-nowrap w-12"></label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-white/70 text-sm font-medium whitespace-nowrap w-12"></label>
<input
type="text"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearchWithReset()}
placeholder="프로젝트, 내용 등"
className="w-44 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
{/* 버튼 영역: 우측 수직 배치 */}
<div className="flex flex-col gap-3">
<button
onClick={handleSearchWithReset}
disabled={loading}
className="h-10 bg-primary-500 hover:bg-primary-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
>
{loading ? (
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
) : (
<Search className="w-4 h-4 mr-2" />
)}
</button>
<button
onClick={openAddModal}
className="h-10 bg-success-500 hover:bg-success-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center"
>
<Plus className="w-4 h-4 mr-2" />
</button>
</div>
</div>
</div>
{/* 중앙: 집계 메뉴 */}
<div className="flex-shrink-0 flex flex-col gap-3 justify-center">
<button
onClick={() => setShowDayReportModal(true)}
className="h-10 bg-indigo-500 hover:bg-indigo-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center whitespace-nowrap"
>
<Calendar className="w-4 h-4 mr-2" />
</button>
<button
onClick={() => setShowTypeReportModal(true)}
className="h-10 bg-purple-500 hover:bg-purple-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center whitespace-nowrap"
>
<FileText className="w-4 h-4 mr-2" />
</button>
</div>
{/* 미등록 업무일지 카드 */}
<div className="flex-shrink-0 w-40">
<div
className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center cursor-pointer hover:bg-white/20 transition-colors"
onClick={() => setShowUnregisteredModal(true)}
>
<div className="text-white/70 text-sm font-medium mb-2 text-center flex items-center justify-center gap-2">
<AlertTriangle className="w-4 h-4 text-danger-400" />
</div>
<div className="text-center">
<span className="text-3xl font-bold text-danger-400">{unregisteredJobReportCount}</span>
<span className="text-white/70 text-lg ml-1"></span>
</div>
</div>
</div>
{/* 우측: 오늘 근무시간 */}
<div className="flex-shrink-0 w-40">
<div className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center">
<div className="text-white/70 text-sm font-medium mb-2 text-center"> </div>
<div className="text-center">
<span className="text-3xl font-bold text-white">{todayWork.hrs.toFixed(1)}</span>
<span className="text-white/70 text-lg ml-1"></span>
</div>
{todayWork.ot > 0 && (
<div className="text-center mt-1">
<span className="text-warning-400 text-sm">OT: {todayWork.ot.toFixed(1)}</span>
</div>
)}
</div>
</div>
</div>
</div>
{/* 데이터 테이블 */}
<div className="glass-effect rounded-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white flex items-center">
<FileText className="w-5 h-5 mr-2" />
</h3>
<span className="text-white/60 text-sm">{jobreportList.length}</span>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-white/10">
<tr>
<th className="px-2 py-3 text-center text-xs font-medium text-white/70 uppercase w-10"></th>
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase w-24"></th>
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase" style={{ width: '35%' }}></th>
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase"></th>
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase"></th>
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase"></th>
{canViewOT && <th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">OT</th>}
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase"></th>
</tr>
</thead>
<tbody className="divide-y divide-white/10">
{loading ? (
<tr>
<td colSpan={canViewOT ? 8 : 7} className="px-4 py-8 text-center">
<div className="flex items-center justify-center">
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
<span className="text-white/50"> ...</span>
</div>
</td>
</tr>
) : jobreportList.length === 0 ? (
<tr>
<td colSpan={canViewOT ? 8 : 7} className="px-4 py-8 text-center text-white/50">
.
</td>
</tr>
) : (
paginatedList.map((item) => (
<tr
key={item.idx}
className="hover:bg-white/5 transition-colors"
>
<td
className="px-2 py-3 text-center cursor-pointer hover:bg-primary-500/10 transition-colors"
onClick={(e) => openCopyModal(item, e)}
title="복사하여 새로 작성"
>
<Copy className="w-4 h-4 mx-auto text-white/40" />
</td>
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{formatDate(item.pdate)}</td>
<td className={`px-4 py-3 text-sm font-medium ${item.pidx && item.pidx > 0 ? 'text-white' : 'text-white/50'}`}>
<div className="flex items-center space-x-2">
{item.pidx && item.pidx > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
window.open(`#/project-detail/${item.pidx}`, '_blank');
}}
className="text-primary-400 hover:text-primary-300 transition-colors flex-shrink-0"
title="프로젝트 정보 보기"
>
<Info className="w-4 h-4" />
</button>
)}
<span className="truncate cursor-pointer" onClick={() => openEditModal(item)} title={item.projectName}>
{item.projectName || '-'}
</span>
</div>
</td>
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{item.type || '-'}</td>
<td className="px-4 py-3 text-sm cursor-pointer" onClick={() => openEditModal(item)}>
<span className={`px-2 py-1 rounded text-xs ${item.status?.includes('완료') ? 'bg-green-500/20 text-green-400' : 'bg-white/20 text-white/70'
}`}>
{item.status || '-'}
</span>
</td>
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>
{item.hrs || 0}
</td>
{canViewOT && (
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>
{item.ot ? <span className="text-warning-400">{item.ot}</span> : '-'}
</td>
)}
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{item.name || item.id || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* 페이징 */}
{totalPages > 1 && (
<div className="px-6 py-4 border-t border-white/10 flex items-center justify-between">
<div className="text-white/50 text-sm">
{jobreportList.length} {(currentPage - 1) * pageSize + 1}-{Math.min(currentPage * pageSize, jobreportList.length)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
«
</button>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
</button>
<span className="text-white/70 px-3">
{currentPage} / {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
»
</button>
</div>
</div>
)}
</div>
{/* 추가/수정 모달 */}
<JobreportEditModal
isOpen={showModal}
editingItem={editingItem}
formData={formData}
processing={processing}
onClose={() => setShowModal(false)}
onFormChange={setFormData}
onSave={handleSave}
onDelete={(idx) => {
handleDelete(idx);
setShowModal(false);
}}
/>
{/* 일별 집계 모달 */}
<JobReportDayDialog
isOpen={showDayReportModal}
onClose={() => setShowDayReportModal(false)}
initialMonth={startDate.substring(0, 7)}
/>
{/* 업무형태별 집계 모달 */}
<JobreportTypeModal
isOpen={showTypeReportModal}
onClose={() => setShowTypeReportModal(false)}
startDate={startDate}
endDate={endDate}
userId={selectedUser}
/>
{/* 업무일지 미등록 상세 모달 */}
{showUnregisteredModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-slate-900 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-danger-400" />
</h3>
<button
onClick={() => setShowUnregisteredModal(false)}
className="text-white/50 hover:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 max-h-[60vh] overflow-y-auto">
<p className="text-white/70 text-sm mb-4">
15( ) 8 .
</p>
{unregisteredJobReportDays.length === 0 ? (
<div className="text-center py-8 text-white/50">
.
</div>
) : (
<div className="space-y-2">
{unregisteredJobReportDays.map((day, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-danger-500/20 flex items-center justify-center text-danger-400 text-xs font-bold">
{index + 1}
</div>
<span className="text-white font-medium">{day.date}</span>
</div>
<div className="flex items-center gap-2">
<span className={`font-bold ${day.hrs === 0 ? 'text-danger-400' : 'text-warning-400'}`}>
{day.hrs}
</span>
<span className="text-white/40 text-xs">/ 8</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
<button
onClick={() => setShowUnregisteredModal(false)}
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm font-medium"
>
</button>
</div>
</div>
</div>
)}
</div>
);
}