차량 관리 (신규): - VehicleList/VehicleDetail: 차량 목록/상세 - ForkliftList/ForkliftDetail: 지게차 목록/상세 - VehicleLogList/VehicleLogDetail: 운행일지 목록/상세 - 관련 페이지 라우트 추가 (/vehicle-management/*) CEO 대시보드: - Enhanced 섹션 컴포넌트 적용 (아이콘 + 컬러 테마) - EnhancedStatusBoardSection, EnhancedDailyReportSection, EnhancedMonthlyExpenseSection - TodayIssueSection 개선 IntegratedDetailTemplate: - FieldInput, FieldRenderer 기능 확장 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 차량 관리 리스트 - UniversalListPage 기반
|
|
* 레거시 5130 사이트 컬럼 구조 기반 (2025-01-28 스크린샷 검증)
|
|
*/
|
|
|
|
import { useState, useMemo, useCallback, useTransition } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Car, Edit, Trash2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { TableRow, TableCell } from '@/components/ui/table';
|
|
import {
|
|
UniversalListPage,
|
|
type UniversalListConfig,
|
|
type SelectionHandlers,
|
|
type RowClickHandlers,
|
|
type ListParams,
|
|
} from '@/components/templates/UniversalListPage';
|
|
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
|
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import { toast } from 'sonner';
|
|
import type { Vehicle } from '../types';
|
|
import { getVehicles, deleteVehicle, bulkDeleteVehicles } from './actions';
|
|
|
|
interface VehicleListProps {
|
|
initialData: Vehicle[];
|
|
}
|
|
|
|
export function VehicleList({ initialData }: VehicleListProps) {
|
|
const router = useRouter();
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
|
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[]>([]);
|
|
const [allData, setAllData] = useState<Vehicle[]>(initialData);
|
|
|
|
const handleView = useCallback(
|
|
(vehicle: Vehicle) => {
|
|
router.push(`/vehicle-management/vehicle/${vehicle.id}`);
|
|
},
|
|
[router]
|
|
);
|
|
|
|
const handleEdit = useCallback(
|
|
(vehicle: Vehicle) => {
|
|
router.push(`/vehicle-management/vehicle/${vehicle.id}/edit`);
|
|
},
|
|
[router]
|
|
);
|
|
|
|
const handleDeleteClick = useCallback((id: string) => {
|
|
setDeleteTargetId(id);
|
|
setIsDeleteDialogOpen(true);
|
|
}, []);
|
|
|
|
const handleConfirmDelete = useCallback(async () => {
|
|
if (!deleteTargetId) return;
|
|
|
|
startTransition(async () => {
|
|
const result = await deleteVehicle(deleteTargetId);
|
|
|
|
if (result.success) {
|
|
const vehicle = allData.find((v) => v.id === deleteTargetId);
|
|
setAllData(allData.filter((v) => v.id !== deleteTargetId));
|
|
toast.success(`차량이 삭제되었습니다${vehicle ? `: ${vehicle.vehicleNumber}` : ''}`);
|
|
window.location.reload();
|
|
} else {
|
|
toast.error(result.error || '삭제에 실패했습니다.');
|
|
}
|
|
|
|
setIsDeleteDialogOpen(false);
|
|
setDeleteTargetId(null);
|
|
});
|
|
}, [deleteTargetId, allData]);
|
|
|
|
const handleBulkDelete = useCallback((selectedIds: string[]) => {
|
|
if (selectedIds.length === 0) {
|
|
toast.error('삭제할 항목을 선택해주세요');
|
|
return;
|
|
}
|
|
setBulkDeleteIds(selectedIds);
|
|
setIsBulkDeleteDialogOpen(true);
|
|
}, []);
|
|
|
|
const handleConfirmBulkDelete = useCallback(async () => {
|
|
startTransition(async () => {
|
|
const result = await bulkDeleteVehicles(bulkDeleteIds);
|
|
|
|
if (result.success) {
|
|
setAllData(allData.filter((v) => !bulkDeleteIds.includes(v.id)));
|
|
toast.success(`${bulkDeleteIds.length}개의 차량이 삭제되었습니다`);
|
|
window.location.reload();
|
|
} else {
|
|
toast.error(result.error || '일괄 삭제에 실패했습니다.');
|
|
}
|
|
|
|
setIsBulkDeleteDialogOpen(false);
|
|
setBulkDeleteIds([]);
|
|
});
|
|
}, [bulkDeleteIds, allData]);
|
|
|
|
const config: UniversalListConfig<Vehicle> = useMemo(
|
|
() => ({
|
|
title: '차량 관리',
|
|
description: '차량 정보를 관리합니다',
|
|
icon: Car,
|
|
basePath: '/vehicle-management/vehicle',
|
|
|
|
idField: 'id',
|
|
getItemId: (item: Vehicle) => item.id,
|
|
|
|
actions: {
|
|
getList: async (params?: ListParams) => {
|
|
try {
|
|
const result = await getVehicles({
|
|
page: params?.page || 1,
|
|
perPage: params?.perPage || 20,
|
|
search: params?.search || undefined,
|
|
});
|
|
|
|
if (result.success) {
|
|
setAllData(result.data);
|
|
return {
|
|
success: true,
|
|
data: result.data,
|
|
totalCount: result.totalCount,
|
|
totalPages: result.totalPages,
|
|
};
|
|
}
|
|
return { success: false, error: result.error || '데이터 조회에 실패했습니다.' };
|
|
} catch {
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
},
|
|
deleteItem: async (id: string) => {
|
|
const result = await deleteVehicle(id);
|
|
return { success: result.success, error: result.error };
|
|
},
|
|
deleteBulk: async (ids: string[]) => {
|
|
const result = await bulkDeleteVehicles(ids);
|
|
return { success: result.success, error: result.error };
|
|
},
|
|
},
|
|
|
|
// 레거시 컬럼 구조 (5130 기준) - 12개 컬럼
|
|
columns: [
|
|
{ key: 'rowNumber', label: '번호', className: 'w-[50px] text-center' },
|
|
{ key: 'vehicleNumber', label: '차량번호', className: 'min-w-[120px]' },
|
|
{ key: 'managerMain', label: '담당자', className: 'w-[80px]' },
|
|
{ key: 'insuranceCompany', label: '보험사', className: 'w-[100px]' },
|
|
{ key: 'insuranceContact', label: '보험사 연락처', className: 'w-[110px]' },
|
|
{ key: 'firstRegistrationDate', label: '최초등록일', className: 'w-[90px]' },
|
|
{ key: 'purchaseDate', label: '구매일자', className: 'w-[90px]' },
|
|
{ key: 'purchaseType', label: '구매 유형', className: 'w-[80px]' },
|
|
{ key: 'oilChangeCycle', label: '엔진오일 교환 주기', className: 'w-[110px]' },
|
|
{ key: 'oilChangeRecords', label: '엔진오일교환일', className: 'min-w-[150px]' },
|
|
{ key: 'maintenanceRecords', label: '정비 정보', className: 'min-w-[180px]' },
|
|
{ key: 'remarks', label: '비고', className: 'min-w-[120px]' },
|
|
],
|
|
|
|
clientSideFiltering: true,
|
|
itemsPerPage: 20,
|
|
|
|
searchFilter: (item: Vehicle, searchValue: string) => {
|
|
const search = searchValue.toLowerCase();
|
|
return (
|
|
item.vehicleNumber.toLowerCase().includes(search) ||
|
|
item.vehicleType.toLowerCase().includes(search) ||
|
|
item.managerMain.toLowerCase().includes(search) ||
|
|
item.insuranceCompany.toLowerCase().includes(search)
|
|
);
|
|
},
|
|
|
|
tabs: [],
|
|
searchPlaceholder: '차량번호, 차종, 담당자, 보험사 검색...',
|
|
|
|
headerActions: () => (
|
|
<Button
|
|
className="ml-auto"
|
|
onClick={() => router.push('/vehicle-management/vehicle/new')}
|
|
>
|
|
<Car className="w-4 h-4 mr-2" />
|
|
차량 등록
|
|
</Button>
|
|
),
|
|
|
|
onBulkDelete: handleBulkDelete,
|
|
|
|
renderTableRow: (
|
|
vehicle: Vehicle,
|
|
index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Vehicle>
|
|
) => {
|
|
// 엔진오일 교환일 포맷 (5130 형식: "날짜, 주행거리 : XX km")
|
|
const oilChangeText = vehicle.oilChangeRecords && vehicle.oilChangeRecords.length > 0
|
|
? vehicle.oilChangeRecords.map(r =>
|
|
`${r.date}, 주행거리 : ${r.mileage || ''} km`
|
|
).join('\n')
|
|
: '정보 없음';
|
|
|
|
// 정비 정보 포맷 (5130 형식: "날짜: 내용")
|
|
const maintenanceText = vehicle.maintenanceRecords && vehicle.maintenanceRecords.length > 0
|
|
? vehicle.maintenanceRecords.map(r =>
|
|
`${r.date}: ${r.description}`
|
|
).join('\n')
|
|
: '정보 없음';
|
|
|
|
return (
|
|
<TableRow
|
|
key={vehicle.id}
|
|
className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`}
|
|
onClick={() => handleView(vehicle)}
|
|
>
|
|
<TableCell onClick={(e) => e.stopPropagation()} className="text-center">
|
|
<Checkbox
|
|
checked={handlers.isSelected}
|
|
onCheckedChange={handlers.onToggle}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
|
<TableCell className="font-medium">{vehicle.vehicleNumber}</TableCell>
|
|
<TableCell>{vehicle.managerMain || '-'}</TableCell>
|
|
<TableCell>{vehicle.insuranceCompany || '-'}</TableCell>
|
|
<TableCell>{vehicle.insuranceContact || '-'}</TableCell>
|
|
<TableCell>{vehicle.firstRegistrationDate || '-'}</TableCell>
|
|
<TableCell>{vehicle.purchaseDate || '-'}</TableCell>
|
|
<TableCell>{vehicle.purchaseType || '-'}</TableCell>
|
|
<TableCell>{vehicle.oilChangeCycle || '정보 없음'}</TableCell>
|
|
<TableCell className="whitespace-pre-line text-xs">{oilChangeText}</TableCell>
|
|
<TableCell className="whitespace-pre-line text-xs">{maintenanceText}</TableCell>
|
|
<TableCell className="whitespace-pre-line text-xs">{vehicle.remarks || '-'}</TableCell>
|
|
</TableRow>
|
|
);
|
|
},
|
|
|
|
renderMobileCard: (
|
|
vehicle: Vehicle,
|
|
index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Vehicle>
|
|
) => {
|
|
return (
|
|
<ListMobileCard
|
|
key={vehicle.id}
|
|
id={vehicle.id}
|
|
isSelected={handlers.isSelected}
|
|
onToggleSelection={handlers.onToggle}
|
|
onClick={() => handleView(vehicle)}
|
|
title={`${vehicle.vehicleNumber} ${vehicle.vehicleType}`}
|
|
subtitle={vehicle.purchaseType}
|
|
infoGrid={
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
<InfoField label="담당자" value={vehicle.managerMain} />
|
|
<InfoField label="보험사" value={vehicle.insuranceCompany || '-'} />
|
|
<InfoField label="보험사 연락처" value={vehicle.insuranceContact || '-'} />
|
|
<InfoField label="구매 유형" value={vehicle.purchaseType || '-'} />
|
|
<InfoField label="최초등록일" value={vehicle.firstRegistrationDate || '-'} />
|
|
<InfoField label="구매일자" value={vehicle.purchaseDate || '-'} />
|
|
</div>
|
|
}
|
|
actions={
|
|
handlers.isSelected ? (
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Button
|
|
variant="default"
|
|
size="default"
|
|
className="flex-1 min-w-[100px] h-11"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEdit(vehicle);
|
|
}}
|
|
>
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
수정
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="default"
|
|
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-transparent"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDeleteClick(vehicle.id);
|
|
}}
|
|
disabled={isPending}
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
삭제
|
|
</Button>
|
|
</div>
|
|
) : undefined
|
|
}
|
|
/>
|
|
);
|
|
},
|
|
}),
|
|
[router, handleView, handleEdit, handleDeleteClick, handleBulkDelete, isPending]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<UniversalListPage config={config} initialData={initialData} />
|
|
|
|
<DeleteConfirmDialog
|
|
open={isDeleteDialogOpen}
|
|
onOpenChange={setIsDeleteDialogOpen}
|
|
description={
|
|
<>
|
|
{deleteTargetId
|
|
? `차량번호: ${allData.find((v) => v.id === deleteTargetId)?.vehicleNumber || deleteTargetId}`
|
|
: ''}
|
|
<br />
|
|
이 차량을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
|
</>
|
|
}
|
|
loading={isPending}
|
|
onConfirm={handleConfirmDelete}
|
|
/>
|
|
|
|
<DeleteConfirmDialog
|
|
open={isBulkDeleteDialogOpen}
|
|
onOpenChange={setIsBulkDeleteDialogOpen}
|
|
description={
|
|
<>
|
|
선택한 {bulkDeleteIds.length}개의 차량을 삭제하시겠습니까?
|
|
<br />
|
|
삭제된 데이터는 복구할 수 없습니다.
|
|
</>
|
|
}
|
|
loading={isPending}
|
|
onConfirm={handleConfirmBulkDelete}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default VehicleList;
|