diff options
| -rw-r--r-- | lib/material/vendor-material/simple-vendor-materials.tsx | 138 | ||||
| -rw-r--r-- | lib/material/vendor-possible-material-service.ts | 65 |
2 files changed, 181 insertions, 22 deletions
diff --git a/lib/material/vendor-material/simple-vendor-materials.tsx b/lib/material/vendor-material/simple-vendor-materials.tsx index 73fdfd18..5b5128c4 100644 --- a/lib/material/vendor-material/simple-vendor-materials.tsx +++ b/lib/material/vendor-material/simple-vendor-materials.tsx @@ -6,9 +6,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { ArrowUpDown, Search } from "lucide-react"; -import { VendorPossibleMaterial } from "../vendor-possible-material-service"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ArrowUpDown, Search, Trash2 } from "lucide-react"; +import { VendorPossibleMaterial, removeConfirmedMaterials } from "../vendor-possible-material-service"; import { AddConfirmedMaterial } from "./add-confirmed-material"; +import { toast } from "sonner"; +import { useTransition } from "react"; // formatDate 함수 제거 - YYYY-MM-DD 형식으로 직접 포맷 interface SimpleVendorMaterialsProps { @@ -37,22 +40,16 @@ export function SimpleVendorMaterials({ if (!date) return "-"; return date.toISOString().split('T')[0]; }; + + // 선택 상태 관리 + const [selectedConfirmedIds, setSelectedConfirmedIds] = useState<number[]>([]); + const [isDeletePending, startDeleteTransition] = useTransition(); + const [confirmedSearch, setConfirmedSearch] = useState(""); const [vendorInputSearch, setVendorInputSearch] = useState(""); const [confirmedSort, setConfirmedSort] = useState<SortState>({ field: null, direction: "asc" }); const [vendorInputSort, setVendorInputSort] = useState<SortState>({ field: null, direction: "asc" }); - // 검색 필터링 함수 - const filterMaterials = (materials: VendorPossibleMaterial[], searchTerm: string) => { - if (!searchTerm.trim()) return materials; - - const lowercaseSearch = searchTerm.toLowerCase(); - return materials.filter(material => - (material.itemCode?.toLowerCase().includes(lowercaseSearch)) || - (material.itemName?.toLowerCase().includes(lowercaseSearch)) - ); - }; - // 정렬 함수 const sortMaterials = (materials: VendorPossibleMaterial[], sortState: SortState) => { if (!sortState.field) return materials; @@ -104,7 +101,18 @@ export function SimpleVendorMaterials({ }); }; - // 필터링 및 정렬된 데이터 + + // 검색 필터링 함수 + const filterMaterials = (materials: VendorPossibleMaterial[], searchTerm: string) => { + if (!searchTerm.trim()) return materials; + + const lowercaseSearch = searchTerm.toLowerCase(); + return materials.filter(material => + (material.itemCode?.toLowerCase().includes(lowercaseSearch)) || + (material.itemName?.toLowerCase().includes(lowercaseSearch)) + ); + }; + const filteredAndSortedConfirmed = useMemo(() => { const filtered = filterMaterials(confirmedMaterials, confirmedSearch); return sortMaterials(filtered, confirmedSort); @@ -115,6 +123,63 @@ export function SimpleVendorMaterials({ return sortMaterials(filtered, vendorInputSort); }, [vendorInputMaterials, vendorInputSearch, vendorInputSort]); + // 선택 상태 관리 함수들 (필터링된 데이터 이후에 선언) + const handleSelectAllConfirmed = (checked: boolean) => { + if (checked) { + const allIds = filteredAndSortedConfirmed.map(material => material.id); + setSelectedConfirmedIds(allIds); + } else { + setSelectedConfirmedIds([]); + } + }; + + // 데이터 변경 시 선택 상태 정리 (유효하지 않은 ID들 제거) + React.useEffect(() => { + const validIds = confirmedMaterials.map(material => material.id); + setSelectedConfirmedIds(prev => prev.filter(id => validIds.includes(id))); + }, [confirmedMaterials]); + + // 전체 선택 체크박스의 상태 계산 + const isAllSelected = filteredAndSortedConfirmed.length > 0 && + selectedConfirmedIds.length === filteredAndSortedConfirmed.length && + filteredAndSortedConfirmed.every(material => selectedConfirmedIds.includes(material.id)); + + const isIndeterminate = selectedConfirmedIds.length > 0 && + selectedConfirmedIds.length < filteredAndSortedConfirmed.length; + + const handleSelectConfirmed = (id: number, checked: boolean) => { + if (checked) { + setSelectedConfirmedIds(prev => [...prev, id]); + } else { + setSelectedConfirmedIds(prev => prev.filter(selectedId => selectedId !== id)); + } + }; + + // 삭제 함수 + const handleDeleteSelected = () => { + if (selectedConfirmedIds.length === 0) { + toast.error("삭제할 항목을 선택해주세요."); + return; + } + + startDeleteTransition(async () => { + const result = await removeConfirmedMaterials(vendorId, selectedConfirmedIds); + + if (result.success) { + toast.success(result.message); + setSelectedConfirmedIds([]); // 선택 상태 초기화 + onDataRefresh?.(); // 데이터 새로고침 + } else { + toast.error(result.message); + } + }); + }; + + + + + + // 정렬 핸들러 const handleSort = ( field: SortField, @@ -160,11 +225,25 @@ export function SimpleVendorMaterials({ 구매담당자가 확정한 공급 품목 정보입니다. </CardDescription> </div> - <AddConfirmedMaterial - vendorId={vendorId} - existingConfirmedMaterials={confirmedMaterials} - onMaterialAdded={onDataRefresh} - /> + <div className="flex items-center gap-2"> + {selectedConfirmedIds.length > 0 && ( + <Button + variant="destructive" + size="sm" + onClick={handleDeleteSelected} + disabled={isDeletePending} + className="flex items-center gap-2" + > + <Trash2 className="h-4 w-4" /> + {isDeletePending ? "삭제 중..." : `삭제 (${selectedConfirmedIds.length})`} + </Button> + )} + <AddConfirmedMaterial + vendorId={vendorId} + existingConfirmedMaterials={confirmedMaterials} + onMaterialAdded={onDataRefresh} + /> + </div> </div> <div className="flex items-center space-x-2"> <Search className="h-4 w-4 text-muted-foreground" /> @@ -181,6 +260,18 @@ export function SimpleVendorMaterials({ <Table> <TableHeader className="sticky top-0 bg-background z-10"> <TableRow> + <TableHead className="w-12"> + <Checkbox + checked={isAllSelected} + ref={(el) => { + if (el && 'indeterminate' in el) { + el.indeterminate = isIndeterminate; + } + }} + onCheckedChange={handleSelectAllConfirmed} + aria-label="전체 선택" + /> + </TableHead> <TableHead> <SortButton field="vendorTypeNameEn" @@ -250,13 +341,20 @@ export function SimpleVendorMaterials({ <TableBody> {filteredAndSortedConfirmed.length === 0 ? ( <TableRow> - <TableCell colSpan={8} className="text-center text-muted-foreground"> + <TableCell colSpan={9} className="text-center text-muted-foreground"> {confirmedSearch ? "검색 결과가 없습니다." : "확정된 공급품목이 없습니다."} </TableCell> </TableRow> ) : ( filteredAndSortedConfirmed.map((material) => ( <TableRow key={material.id}> + <TableCell> + <Checkbox + checked={selectedConfirmedIds.includes(material.id)} + onCheckedChange={(checked) => handleSelectConfirmed(material.id, checked as boolean)} + aria-label={`자재 ${material.itemCode} 선택`} + /> + </TableCell> <TableCell>{material.vendorTypeNameEn || "-"}</TableCell> <TableCell className="font-mono">{material.itemCode || "-"}</TableCell> <TableCell>{material.itemName || "-"}</TableCell> diff --git a/lib/material/vendor-possible-material-service.ts b/lib/material/vendor-possible-material-service.ts index 03c914a7..3589cf4e 100644 --- a/lib/material/vendor-possible-material-service.ts +++ b/lib/material/vendor-possible-material-service.ts @@ -1,7 +1,7 @@ "use server"; import { unstable_noStore } from "next/cache"; -import { and, desc, eq, count } from "drizzle-orm"; +import { and, desc, eq, inArray } from "drizzle-orm"; import db from "@/db/db"; import { vendorPossibleMaterials, vendors, vendorTypes } from "@/db/schema/vendors"; @@ -198,10 +198,71 @@ export async function addConfirmedMaterial( .returning(); console.log(`확정정보 자재 추가 성공: vendorId=${vendorId}, itemCode=${materialData.itemCode}, 등록자=${registerUserName}`); - + return result[0]; } catch (error) { console.error("확정정보 자재 추가 실패:", error); throw error; } } + +/** + * 확정정보 자재 삭제 (구매담당자용) + */ +export async function removeConfirmedMaterials( + vendorId: number, + materialIds: number[] +) { + unstable_noStore(); + + try { + if (materialIds.length === 0) { + throw new Error("삭제할 자재가 선택되지 않았습니다."); + } + + // 삭제 전 해당 자재들이 모두 해당 vendor의 확정정보인지 확인 + const materialsToDelete = await db + .select() + .from(vendorPossibleMaterials) + .where( + and( + eq(vendorPossibleMaterials.vendorId, vendorId), + eq(vendorPossibleMaterials.isConfirmed, true), + inArray(vendorPossibleMaterials.id, materialIds) + ) + ); + + if (materialsToDelete.length !== materialIds.length) { + const foundIds = materialsToDelete.map(m => m.id); + const invalidIds = materialIds.filter(id => !foundIds.includes(id)); + throw new Error(`유효하지 않은 자재 ID가 포함되어 있습니다: ${invalidIds.join(", ")}`); + } + + // 선택된 자재들 삭제 + const result = await db + .delete(vendorPossibleMaterials) + .where( + and( + eq(vendorPossibleMaterials.vendorId, vendorId), + eq(vendorPossibleMaterials.isConfirmed, true), + inArray(vendorPossibleMaterials.id, materialIds) + ) + ) + .returning({ id: vendorPossibleMaterials.id }); + + console.log(`확정정보 자재 삭제 성공: vendorId=${vendorId}, 삭제된 건수=${result.length}`); + + return { + success: true, + deletedCount: result.length, + message: `${result.length}개의 자재가 삭제되었습니다.` + }; + } catch (error) { + console.error("확정정보 자재 삭제 실패:", error); + return { + success: false, + deletedCount: 0, + message: error instanceof Error ? error.message : "자재 삭제 중 오류가 발생했습니다." + }; + } +}
\ No newline at end of file |
