summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/material/vendor-material/simple-vendor-materials.tsx138
-rw-r--r--lib/material/vendor-possible-material-service.ts65
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