feat(WEB): UniversalListPage 전체 마이그레이션 및 코드 정리
- UniversalListPage/IntegratedListTemplateV2 컴포넌트 기능 개선 - 회계, HR, 건설, 고객센터, 결재, 설정 등 전체 리스트 컴포넌트 마이그레이션 - 테스트 페이지 및 미사용 API 라우트 정리 (board-test, order-management-test 등) - 미들웨어 토큰 갱신 로직 개선 - AuthenticatedLayout 구조 개선 - claudedocs 문서 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
Download,
|
||||
Plus,
|
||||
Calendar,
|
||||
Check,
|
||||
@@ -40,13 +39,14 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
IntegratedListTemplateV2,
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type TableColumn,
|
||||
type StatCard,
|
||||
type TabOption,
|
||||
type FilterFieldConfig,
|
||||
type FilterValues,
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { VacationGrantDialog } from './VacationGrantDialog';
|
||||
@@ -109,7 +109,6 @@ export function VacationManagement() {
|
||||
const [filterOption, setFilterOption] = useState<FilterOption>('all');
|
||||
const [sortOption, setSortOption] = useState<SortOption>('rank');
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 날짜 범위 상태 (input type="date" 용)
|
||||
@@ -263,28 +262,8 @@ export function VacationManagement() {
|
||||
setMainTab(value as MainTabType);
|
||||
setSelectedItems(new Set());
|
||||
setSearchQuery('');
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
// ===== 체크박스 핸들러 =====
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) newSet.delete(id);
|
||||
else newSet.add(id);
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
const currentData = mainTab === 'usage' ? filteredUsageData : mainTab === 'grant' ? filteredGrantData : filteredRequestData;
|
||||
if (selectedItems.size === currentData.length && currentData.length > 0) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(currentData.map(item => item.id)));
|
||||
}
|
||||
}, [mainTab, selectedItems.size]);
|
||||
|
||||
// ===== 필터링된 데이터 =====
|
||||
const filteredUsageData = useMemo(() => {
|
||||
return usageData.filter(item =>
|
||||
@@ -317,18 +296,12 @@ export function VacationManagement() {
|
||||
}
|
||||
}, [mainTab, filteredUsageData, filteredGrantData, filteredRequestData]);
|
||||
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
return currentData.slice(startIndex, startIndex + itemsPerPage);
|
||||
}, [currentData, currentPage, itemsPerPage]);
|
||||
|
||||
const totalPages = Math.ceil(currentData.length / itemsPerPage);
|
||||
|
||||
// ===== 승인/거절 핸들러 =====
|
||||
const handleApproveClick = useCallback(() => {
|
||||
if (selectedItems.size === 0) return;
|
||||
const handleApproveClick = useCallback((selected: Set<string>) => {
|
||||
// 버튼 클릭 시 UniversalListPage의 선택 상태를 내부 state로 복사
|
||||
setSelectedItems(selected);
|
||||
setApproveDialogOpen(true);
|
||||
}, [selectedItems.size]);
|
||||
}, []);
|
||||
|
||||
const handleApproveConfirm = useCallback(async () => {
|
||||
if (isProcessing) return;
|
||||
@@ -352,10 +325,11 @@ export function VacationManagement() {
|
||||
}
|
||||
}, [selectedItems, isProcessing, fetchLeaveRequests, fetchUsageData]);
|
||||
|
||||
const handleRejectClick = useCallback(() => {
|
||||
if (selectedItems.size === 0) return;
|
||||
const handleRejectClick = useCallback((selected: Set<string>) => {
|
||||
// 버튼 클릭 시 UniversalListPage의 선택 상태를 내부 state로 복사
|
||||
setSelectedItems(selected);
|
||||
setRejectDialogOpen(true);
|
||||
}, [selectedItems.size]);
|
||||
}, []);
|
||||
|
||||
const handleRejectConfirm = useCallback(async () => {
|
||||
if (isProcessing) return;
|
||||
@@ -455,22 +429,28 @@ export function VacationManagement() {
|
||||
}, [mainTab]);
|
||||
|
||||
// ===== 테이블 행 렌더링 =====
|
||||
const renderTableRow = useCallback((item: any, index: number, globalIndex: number) => {
|
||||
const isSelected = selectedItems.has(item.id);
|
||||
const renderTableRow = useCallback((
|
||||
item: any,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
if (mainTab === 'usage') {
|
||||
const record = item as VacationUsageRecord;
|
||||
const hireDateStr = record.hireDate && record.hireDate !== '-' ? format(new Date(record.hireDate), 'yyyy-MM-dd') : '-';
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<Checkbox checked={isSelected} onCheckedChange={() => toggleSelection(record.id)} />
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
<TableCell>{record.department}</TableCell>
|
||||
<TableCell>{record.position}</TableCell>
|
||||
<TableCell className="font-medium">{record.employeeName}</TableCell>
|
||||
<TableCell>{record.rank}</TableCell>
|
||||
<TableCell>{record.hireDate !== '-' ? format(new Date(record.hireDate), 'yyyy-MM-dd') : '-'}</TableCell>
|
||||
<TableCell>{hireDateStr}</TableCell>
|
||||
<TableCell className="text-center">{record.baseVacation}</TableCell>
|
||||
<TableCell className="text-center">{record.grantedVacation}</TableCell>
|
||||
<TableCell className="text-center">{record.usedVacation}</TableCell>
|
||||
@@ -479,10 +459,11 @@ export function VacationManagement() {
|
||||
);
|
||||
} else if (mainTab === 'grant') {
|
||||
const record = item as VacationGrantRecord;
|
||||
const grantDateStr = record.grantDate ? format(new Date(record.grantDate), 'yyyy-MM-dd') : '-';
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<Checkbox checked={isSelected} onCheckedChange={() => toggleSelection(record.id)} />
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
<TableCell>{record.department}</TableCell>
|
||||
@@ -492,17 +473,20 @@ export function VacationManagement() {
|
||||
<TableCell>
|
||||
<Badge variant="outline">{VACATION_TYPE_LABELS[record.vacationType]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(record.grantDate), 'yyyy-MM-dd')}</TableCell>
|
||||
<TableCell>{grantDateStr}</TableCell>
|
||||
<TableCell className="text-center">{record.grantDays}일</TableCell>
|
||||
<TableCell className="text-muted-foreground">{record.reason || '-'}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
} else {
|
||||
const record = item as VacationRequestRecord;
|
||||
const startDateStr = record.startDate ? format(new Date(record.startDate), 'yyyy-MM-dd') : '-';
|
||||
const endDateStr = record.endDate ? format(new Date(record.endDate), 'yyyy-MM-dd') : '-';
|
||||
const requestDateStr = record.requestDate ? format(new Date(record.requestDate), 'yyyy-MM-dd') : '-';
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<Checkbox checked={isSelected} onCheckedChange={() => toggleSelection(record.id)} />
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
<TableCell>{record.department}</TableCell>
|
||||
@@ -510,7 +494,7 @@ export function VacationManagement() {
|
||||
<TableCell className="font-medium">{record.employeeName}</TableCell>
|
||||
<TableCell>{record.rank}</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(record.startDate), 'yyyy-MM-dd')} ~ {format(new Date(record.endDate), 'yyyy-MM-dd')}
|
||||
{startDateStr} ~ {endDateStr}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{record.vacationDays}일</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -518,20 +502,20 @@ export function VacationManagement() {
|
||||
{REQUEST_STATUS_LABELS[record.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(record.requestDate), 'yyyy-MM-dd')}</TableCell>
|
||||
<TableCell>{requestDateStr}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}, [mainTab, selectedItems, toggleSelection]);
|
||||
}, [mainTab]);
|
||||
|
||||
// ===== 모바일 카드 렌더링 =====
|
||||
const renderMobileCard = useCallback((
|
||||
item: any,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
isSelected: boolean,
|
||||
onToggle: () => void
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
if (mainTab === 'usage') {
|
||||
const record = item as VacationUsageRecord;
|
||||
return (
|
||||
@@ -598,10 +582,10 @@ export function VacationManagement() {
|
||||
actions={
|
||||
record.status === 'pending' && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="default" className="flex-1" onClick={handleApproveClick}>
|
||||
<Button variant="default" className="flex-1" onClick={() => handleApproveClick(new Set([record.id]))}>
|
||||
<Check className="w-4 h-4 mr-2" /> 승인
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={handleRejectClick}>
|
||||
<Button variant="outline" className="flex-1" onClick={() => handleRejectClick(new Set([record.id]))}>
|
||||
<X className="w-4 h-4 mr-2" /> 거절
|
||||
</Button>
|
||||
</div>
|
||||
@@ -613,7 +597,7 @@ export function VacationManagement() {
|
||||
}, [mainTab, handleApproveClick, handleRejectClick]);
|
||||
|
||||
// ===== 헤더 액션 (DateRangeSelector + 버튼들) =====
|
||||
const headerActions = (
|
||||
const headerActions = useCallback(({ selectedItems: selected }: { selectedItems: Set<string>; onClearSelection?: () => void; onRefresh?: () => void }) => (
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
@@ -633,13 +617,13 @@ export function VacationManagement() {
|
||||
{mainTab === 'request' && (
|
||||
<>
|
||||
{/* 버튼 순서: 승인 → 거절 → 휴가신청 (휴가신청 버튼 위치 고정) */}
|
||||
{selectedItems.size > 0 && (
|
||||
{selected.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleApproveClick}>
|
||||
<Button variant="default" onClick={() => handleApproveClick(selected)}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRejectClick}>
|
||||
<Button variant="destructive" onClick={() => handleRejectClick(selected)}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
거절
|
||||
</Button>
|
||||
@@ -651,15 +635,9 @@ export function VacationManagement() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 엑셀 다운로드 버튼 - 주석처리 */}
|
||||
{/* <Button variant="outline" onClick={() => console.log('엑셀 다운로드')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
), [startDate, endDate, mainTab, handleApproveClick, handleRejectClick]);
|
||||
|
||||
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||
@@ -698,153 +676,193 @@ export function VacationManagement() {
|
||||
setSortOption(value as SortOption);
|
||||
break;
|
||||
}
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
const handleFilterReset = useCallback(() => {
|
||||
setFilterOption('all');
|
||||
setSortOption('rank');
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
// ===== UniversalListPage 설정 =====
|
||||
const vacationConfig: UniversalListConfig<any> = useMemo(() => ({
|
||||
title: '휴가관리',
|
||||
description: '직원들의 휴가 현황을 관리합니다',
|
||||
icon: Calendar,
|
||||
basePath: '/hr/vacation-management',
|
||||
|
||||
idField: 'id',
|
||||
|
||||
actions: {
|
||||
getList: async () => ({
|
||||
success: true,
|
||||
data: currentData,
|
||||
totalCount: currentData.length,
|
||||
}),
|
||||
},
|
||||
|
||||
columns: tableColumns,
|
||||
|
||||
tabs: tabs,
|
||||
defaultTab: mainTab,
|
||||
|
||||
searchPlaceholder: '이름, 부서 검색...',
|
||||
|
||||
itemsPerPage: itemsPerPage,
|
||||
|
||||
clientSideFiltering: true,
|
||||
|
||||
searchFilter: (item, searchValue) => {
|
||||
return (
|
||||
item.employeeName?.includes(searchValue) ||
|
||||
item.department?.includes(searchValue)
|
||||
);
|
||||
},
|
||||
|
||||
// tabFilter 제거: 탭별로 다른 데이터를 사용하므로 원래 tabs.count 유지
|
||||
|
||||
computeStats: () => statCards,
|
||||
|
||||
filterConfig: filterConfig,
|
||||
|
||||
headerActions: headerActions,
|
||||
|
||||
renderTableRow: renderTableRow,
|
||||
|
||||
renderMobileCard: renderMobileCard,
|
||||
|
||||
renderDialogs: () => (
|
||||
<>
|
||||
{/* 휴가 부여 다이얼로그 */}
|
||||
<VacationGrantDialog
|
||||
open={grantDialogOpen}
|
||||
onOpenChange={setGrantDialogOpen}
|
||||
onSave={async (data) => {
|
||||
try {
|
||||
const result = await createLeaveGrant({
|
||||
userId: parseInt(data.employeeId, 10),
|
||||
grantType: data.vacationType as LeaveGrantType,
|
||||
grantDate: data.grantDate,
|
||||
grantDays: data.grantDays,
|
||||
reason: data.reason,
|
||||
});
|
||||
if (result.success) {
|
||||
await fetchGrantData();
|
||||
await fetchUsageData();
|
||||
} else {
|
||||
alert(`휴가 부여 실패: ${result.error}`);
|
||||
console.error('[VacationManagement] 휴가 부여 실패:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[VacationManagement] 휴가 부여 에러:', error);
|
||||
alert('휴가 부여 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setGrantDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 휴가 신청 다이얼로그 */}
|
||||
<VacationRequestDialog
|
||||
open={requestDialogOpen}
|
||||
onOpenChange={setRequestDialogOpen}
|
||||
onSave={async (data) => {
|
||||
try {
|
||||
const result = await createLeave({
|
||||
userId: parseInt(data.employeeId, 10),
|
||||
leaveType: data.leaveType,
|
||||
startDate: data.startDate,
|
||||
endDate: data.endDate,
|
||||
days: data.vacationDays,
|
||||
});
|
||||
if (result.success) {
|
||||
await fetchLeaveRequests();
|
||||
await fetchUsageData();
|
||||
} else {
|
||||
alert(`휴가 신청 실패: ${result.error}`);
|
||||
console.error('[VacationManagement] 휴가 신청 실패:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[VacationManagement] 휴가 신청 에러:', error);
|
||||
alert('휴가 신청 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setRequestDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 승인 확인 다이얼로그 */}
|
||||
<AlertDialog open={approveDialogOpen} onOpenChange={setApproveDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 승인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 승인하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleApproveConfirm}>
|
||||
승인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 거절 확인 다이얼로그 */}
|
||||
<AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 거절</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 거절하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleRejectConfirm}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
거절
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
),
|
||||
}), [
|
||||
currentData,
|
||||
tableColumns,
|
||||
tabs,
|
||||
mainTab,
|
||||
itemsPerPage,
|
||||
statCards,
|
||||
filterConfig,
|
||||
headerActions,
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
grantDialogOpen,
|
||||
requestDialogOpen,
|
||||
approveDialogOpen,
|
||||
rejectDialogOpen,
|
||||
selectedItems.size,
|
||||
handleApproveConfirm,
|
||||
handleRejectConfirm,
|
||||
fetchGrantData,
|
||||
fetchUsageData,
|
||||
fetchLeaveRequests,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* IntegratedListTemplateV2 - 카드 아래에 탭 표시됨 */}
|
||||
<IntegratedListTemplateV2
|
||||
title="휴가관리"
|
||||
description="직원들의 휴가 현황을 관리합니다"
|
||||
icon={Calendar}
|
||||
headerActions={headerActions}
|
||||
stats={statCards}
|
||||
searchValue={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
searchPlaceholder="이름, 부서 검색..."
|
||||
filterConfig={filterConfig}
|
||||
filterValues={filterValues}
|
||||
onFilterChange={handleFilterChange}
|
||||
onFilterReset={handleFilterReset}
|
||||
filterTitle="휴가 필터"
|
||||
tabs={tabs}
|
||||
activeTab={mainTab}
|
||||
onTabChange={handleMainTabChange}
|
||||
tableColumns={tableColumns}
|
||||
data={paginatedData}
|
||||
totalCount={currentData.length}
|
||||
allData={currentData}
|
||||
selectedItems={selectedItems}
|
||||
onToggleSelection={toggleSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
getItemId={(item: any) => item.id}
|
||||
renderTableRow={renderTableRow}
|
||||
renderMobileCard={renderMobileCard}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems: currentData.length,
|
||||
itemsPerPage,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 다이얼로그 */}
|
||||
<VacationGrantDialog
|
||||
open={grantDialogOpen}
|
||||
onOpenChange={setGrantDialogOpen}
|
||||
onSave={async (data) => {
|
||||
try {
|
||||
// VacationGrantFormData를 CreateLeaveGrantRequest 형식으로 변환
|
||||
const result = await createLeaveGrant({
|
||||
userId: parseInt(data.employeeId, 10),
|
||||
grantType: data.vacationType as LeaveGrantType,
|
||||
grantDate: data.grantDate,
|
||||
grantDays: data.grantDays,
|
||||
reason: data.reason,
|
||||
});
|
||||
if (result.success) {
|
||||
await fetchGrantData();
|
||||
await fetchUsageData(); // 잔여휴가도 갱신
|
||||
} else {
|
||||
alert(`휴가 부여 실패: ${result.error}`);
|
||||
console.error('[VacationManagement] 휴가 부여 실패:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[VacationManagement] 휴가 부여 에러:', error);
|
||||
alert('휴가 부여 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setGrantDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<VacationRequestDialog
|
||||
open={requestDialogOpen}
|
||||
onOpenChange={setRequestDialogOpen}
|
||||
onSave={async (data) => {
|
||||
try {
|
||||
// VacationRequestFormData를 CreateLeaveRequest 형식으로 변환
|
||||
const result = await createLeave({
|
||||
userId: parseInt(data.employeeId, 10),
|
||||
leaveType: data.leaveType,
|
||||
startDate: data.startDate,
|
||||
endDate: data.endDate,
|
||||
days: data.vacationDays,
|
||||
});
|
||||
if (result.success) {
|
||||
await fetchLeaveRequests();
|
||||
await fetchUsageData(); // 잔여휴가도 갱신
|
||||
} else {
|
||||
alert(`휴가 신청 실패: ${result.error}`);
|
||||
console.error('[VacationManagement] 휴가 신청 실패:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[VacationManagement] 휴가 신청 에러:', error);
|
||||
alert('휴가 신청 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setRequestDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 승인 확인 다이얼로그 */}
|
||||
<AlertDialog open={approveDialogOpen} onOpenChange={setApproveDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 승인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 승인하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleApproveConfirm}>
|
||||
승인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 거절 확인 다이얼로그 */}
|
||||
<AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 거절</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 거절하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleRejectConfirm}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
거절
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
<UniversalListPage<any>
|
||||
config={vacationConfig}
|
||||
initialData={currentData}
|
||||
initialTotalCount={currentData.length}
|
||||
onTabChange={handleMainTabChange}
|
||||
onSearchChange={setSearchQuery}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user