diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-21 07:54:26 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-21 07:54:26 +0000 |
| commit | 14f61e24947fb92dd71ec0a7196a6e815f8e66da (patch) | |
| tree | 317c501d64662d05914330628f867467fba78132 /lib/tech-vendors/possible-items | |
| parent | 194bd4bd7e6144d5c09c5e3f5476d254234dce72 (diff) | |
(최겸)기술영업 RFQ 담당자 초대, 요구사항 반영
Diffstat (limited to 'lib/tech-vendors/possible-items')
4 files changed, 780 insertions, 0 deletions
diff --git a/lib/tech-vendors/possible-items/add-item-dialog.tsx b/lib/tech-vendors/possible-items/add-item-dialog.tsx new file mode 100644 index 00000000..ef15a5ce --- /dev/null +++ b/lib/tech-vendors/possible-items/add-item-dialog.tsx @@ -0,0 +1,284 @@ +"use client";
+
+import * as React from "react";
+import { Search, X } from "lucide-react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import {
+ getItemsForTechVendor,
+ addTechVendorPossibleItem
+} from "../service";
+
+interface ItemData {
+ id: number;
+ itemCode: string | null;
+ itemList: string | null;
+ workType: string | null;
+ shipTypes?: string | null;
+ subItemList?: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface AddItemDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ vendorId: number;
+}
+
+export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogProps) {
+ // 아이템 관련 상태
+ const [items, setItems] = React.useState<ItemData[]>([]);
+ const [filteredItems, setFilteredItems] = React.useState<ItemData[]>([]);
+ const [itemSearch, setItemSearch] = React.useState("");
+ const [selectedItems, setSelectedItems] = React.useState<ItemData[]>([]);
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ // 다이얼로그가 열릴 때 아이템 목록 로드
+ React.useEffect(() => {
+ if (open && vendorId) {
+ loadItems();
+ }
+ }, [open, vendorId]);
+
+ // 아이템 검색 필터링
+ React.useEffect(() => {
+ if (!itemSearch) {
+ setFilteredItems(items);
+ } else {
+ const filtered = items.filter(item =>
+ item.itemCode?.toLowerCase().includes(itemSearch.toLowerCase()) ||
+ item.itemList?.toLowerCase().includes(itemSearch.toLowerCase()) ||
+ item.workType?.toLowerCase().includes(itemSearch.toLowerCase())
+ );
+ setFilteredItems(filtered);
+ }
+ }, [items, itemSearch]);
+
+ const loadItems = async () => {
+ try {
+ setIsLoading(true);
+ console.log("Loading items for vendor:", vendorId);
+ const result = await getItemsForTechVendor(vendorId);
+
+ if (result.error) {
+ throw new Error(result.error);
+ }
+
+ console.log("Loaded items:", result.data.length, result.data);
+ // itemCode가 null이 아닌 항목만 필터링
+ const validItems = result.data.filter(item => item.itemCode != null);
+ setItems(validItems);
+ } catch (error) {
+ console.error("Failed to load items:", error);
+ toast.error("아이템 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleItemToggle = (item: ItemData) => {
+ if (!item.itemCode) return; // itemCode가 null인 경우 처리하지 않음
+
+ setSelectedItems(prev => {
+ // itemCode + shipTypes 조합으로 중복 체크
+ const isSelected = prev.some(i =>
+ i.itemCode === item.itemCode && i.shipTypes === item.shipTypes
+ );
+ if (isSelected) {
+ return prev.filter(i =>
+ !(i.itemCode === item.itemCode && i.shipTypes === item.shipTypes)
+ );
+ } else {
+ return [...prev, item];
+ }
+ });
+ };
+
+ const handleSubmit = async () => {
+ if (selectedItems.length === 0) return;
+
+ try {
+ setIsLoading(true);
+ let successCount = 0;
+ let errorCount = 0;
+
+ for (const item of selectedItems) {
+ if (!item.itemCode) continue; // itemCode가 null인 경우 건너뛰기
+
+ const result = await addTechVendorPossibleItem({
+ vendorId: vendorId,
+ itemCode: item.itemCode,
+ workType: item.workType || undefined,
+ shipTypes: item.shipTypes || undefined,
+ itemList: item.itemList || undefined,
+ subItemList: item.subItemList || undefined,
+ });
+
+ if (result.success) {
+ successCount++;
+ } else {
+ errorCount++;
+ console.error("Failed to add item:", item.itemCode, result.error);
+ }
+ }
+
+ if (successCount > 0) {
+ toast.success(
+ `${successCount}개의 아이템이 추가되었습니다.${
+ errorCount > 0 ? ` (${errorCount}개 실패)` : ""
+ }`
+ );
+
+ handleClose();
+ } else {
+ toast.error("아이템 추가에 실패했습니다.");
+ }
+ } catch (error) {
+ console.error("Failed to add items:", error);
+ toast.error("아이템 추가 중 오류가 발생했습니다.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ onOpenChange(false);
+ setTimeout(() => {
+ setSelectedItems([]);
+ setItemSearch("");
+ setItems([]);
+ setFilteredItems([]);
+ }, 200);
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>아이템 추가</DialogTitle>
+ <DialogDescription>
+ 추가할 아이템을 선택하세요. 복수 선택이 가능합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 min-h-0 space-y-4">
+ {/* 검색 */}
+ <div className="space-y-2">
+ <Label htmlFor="item-search">아이템 검색</Label>
+ <div className="relative">
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
+ <Input
+ id="item-search"
+ placeholder="아이템코드, 아이템리스트, 공종으로 검색..."
+ value={itemSearch}
+ onChange={(e) => setItemSearch(e.target.value)}
+ className="pl-10"
+ />
+ </div>
+ </div>
+
+ {/* 선택된 아이템 표시 */}
+ {selectedItems.length > 0 && (
+ <div className="space-y-2">
+ <Label>선택된 아이템 ({selectedItems.length}개)</Label>
+ <div className="flex flex-wrap gap-1 p-2 border rounded-md bg-muted/50 max-h-20 overflow-y-auto">
+ {selectedItems.map((item) => {
+ if (!item.itemCode) return null;
+ const itemKey = `${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
+ return (
+ <Badge key={`selected-${itemKey}`} variant="default" className="text-xs">
+ {itemKey}
+ <X
+ className="ml-1 h-3 w-3 cursor-pointer"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleItemToggle(item);
+ }}
+ />
+ </Badge>
+ );
+ })}
+ </div>
+ </div>
+ )}
+
+ {/* 아이템 목록 */}
+ <div className="max-h-96 overflow-y-auto border rounded-lg bg-gray-50 p-2">
+ <div className="space-y-2">
+ {isLoading ? (
+ <div className="text-center py-4">아이템 로딩 중...</div>
+ ) : filteredItems.length === 0 && items.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 해당 벤더 타입에 대한 추가 가능한 아이템이 없습니다.
+ </div>
+ ) : filteredItems.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 검색 결과가 없습니다.
+ </div>
+ ) : (
+ filteredItems.map((item) => {
+ if (!item.itemCode) return null; // itemCode가 null인 경우 렌더링하지 않음
+
+ // itemCode + shipTypes 조합으로 선택 여부 체크
+ const isSelected = selectedItems.some(i =>
+ i.itemCode === item.itemCode && i.shipTypes === item.shipTypes
+ );
+ const itemKey = `${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
+
+ return (
+ <div
+ key={`item-${itemKey}`}
+ className={`p-3 bg-white border rounded-lg cursor-pointer transition-colors ${
+ isSelected
+ ? "bg-primary/10 border-primary hover:bg-primary/20"
+ : "hover:bg-gray-50"
+ }`}
+ onClick={() => handleItemToggle(item)}
+ >
+ <div className="font-medium">
+ {itemKey}
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {item.itemList || "-"}
+ </div>
+ <div className="flex gap-2 mt-1 text-xs">
+ <span>공종: {item.workType || "-"}</span>
+ {item.shipTypes && <span>선종: {item.shipTypes}</span>}
+ {item.subItemList && <span>서브아이템: {item.subItemList}</span>}
+ </div>
+ </div>
+ );
+ })
+ )}
+ </div>
+ </div>
+ </div>
+
+ <div className="flex justify-end gap-2 pt-4 border-t">
+ <Button variant="outline" onClick={handleClose}>
+ 취소
+ </Button>
+ <Button
+ onClick={handleSubmit}
+ disabled={selectedItems.length === 0 || isLoading}
+ >
+ {isLoading ? "추가 중..." : `추가 (${selectedItems.length})`}
+ </Button>
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
\ No newline at end of file diff --git a/lib/tech-vendors/possible-items/possible-items-columns.tsx b/lib/tech-vendors/possible-items/possible-items-columns.tsx new file mode 100644 index 00000000..71bcb3b8 --- /dev/null +++ b/lib/tech-vendors/possible-items/possible-items-columns.tsx @@ -0,0 +1,206 @@ +"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Ellipsis } from "lucide-react"
+
+import { formatDate } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import type { TechVendorPossibleItem } from "../validations"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TechVendorPossibleItem> | null>>;
+}
+
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<TechVendorPossibleItem>[] {
+ return [
+ // 선택 체크박스
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ },
+
+ // 아이템 코드
+ {
+ accessorKey: "itemCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="아이템 코드" />
+ ),
+ cell: ({ row }) => (
+ <div className="font-mono text-sm">
+ {row.getValue("itemCode")}
+ </div>
+ ),
+ size: 150,
+ },
+
+ // 공종
+ {
+ accessorKey: "workType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="공종" />
+ ),
+ cell: ({ row }) => {
+ const workType = row.getValue("workType") as string | null
+ return workType ? (
+ <Badge variant="secondary" className="text-xs">
+ {workType}
+ </Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ size: 100,
+ },
+
+ // 아이템명
+ {
+ accessorKey: "itemList",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="아이템명" />
+ ),
+ cell: ({ row }) => {
+ const itemList = row.getValue("itemList") as string | null
+ return (
+ <div className="max-w-[300px]">
+ {itemList || <span className="text-muted-foreground">-</span>}
+ </div>
+ )
+ },
+ size: 300,
+ },
+
+ // 선종 (조선용)
+ {
+ accessorKey: "shipTypes",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="선종" />
+ ),
+ cell: ({ row }) => {
+ const shipTypes = row.getValue("shipTypes") as string | null
+ return shipTypes ? (
+ <Badge variant="outline" className="text-xs">
+ {shipTypes}
+ </Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ size: 120,
+ },
+
+ // 서브아이템 (해양용)
+ {
+ accessorKey: "subItemList",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="서브아이템" />
+ ),
+ cell: ({ row }) => {
+ const subItemList = row.getValue("subItemList") as string | null
+ return (
+ <div className="max-w-[200px]">
+ {subItemList || <span className="text-muted-foreground">-</span>}
+ </div>
+ )
+ },
+ size: 200,
+ },
+
+ // 등록일
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등록일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("createdAt") as Date
+ return (
+ <div className="text-sm text-muted-foreground">
+ {formatDate(date)}
+ </div>
+ )
+ },
+ size: 120,
+ },
+
+ // 수정일
+ {
+ accessorKey: "updatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="수정일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("updatedAt") as Date
+ return (
+ <div className="text-sm text-muted-foreground">
+ {formatDate(date)}
+ </div>
+ )
+ },
+ size: 120,
+ },
+
+ // 액션 메뉴
+ {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "delete" })}
+ >
+ 삭제
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 40,
+ },
+ ]
+}
\ No newline at end of file diff --git a/lib/tech-vendors/possible-items/possible-items-table.tsx b/lib/tech-vendors/possible-items/possible-items-table.tsx new file mode 100644 index 00000000..9c024a93 --- /dev/null +++ b/lib/tech-vendors/possible-items/possible-items-table.tsx @@ -0,0 +1,171 @@ +"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { toast } from "sonner"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+
+import { getColumns } from "./possible-items-columns"
+import {
+ getTechVendorPossibleItems,
+ deleteTechVendorPossibleItem,
+} from "../service"
+import type { TechVendorPossibleItem } from "../validations"
+import { PossibleItemsTableToolbarActions } from "./possible-items-toolbar-actions"
+import { AddItemDialog } from "./add-item-dialog"
+
+interface TechVendorPossibleItemsTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getTechVendorPossibleItems>>,
+ ]
+ >
+ vendorId: number
+}
+
+export function TechVendorPossibleItemsTable({
+ promises,
+ vendorId,
+}: TechVendorPossibleItemsTableProps) {
+ // Suspense로 받아온 데이터
+ const [{ data, pageCount }] = React.use(promises)
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<TechVendorPossibleItem> | null>(null)
+ const [showAddDialog, setShowAddDialog] = React.useState(false)
+ const [showDeleteAlert, setShowDeleteAlert] = React.useState(false)
+ const [isDeleting, setIsDeleting] = React.useState(false)
+
+ // getColumns() 호출 시, setRowAction을 주입
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
+
+ // 단일 아이템 삭제 핸들러
+ async function handleDeleteItem() {
+ if (!rowAction || rowAction.type !== "delete") return
+
+ setIsDeleting(true)
+ try {
+ const { success, error } = await deleteTechVendorPossibleItem(
+ rowAction.row.original.id,
+ vendorId
+ )
+
+ if (!success) {
+ throw new Error(error)
+ }
+
+ toast.success("아이템이 삭제되었습니다")
+ setShowDeleteAlert(false)
+ setRowAction(null)
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : "아이템 삭제 중 오류가 발생했습니다")
+ } finally {
+ setIsDeleting(false)
+ }
+ }
+
+ const filterFields: DataTableFilterField<TechVendorPossibleItem>[] = [
+ { id: "itemCode", label: "아이템 코드" },
+ { id: "workType", label: "공종" },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<TechVendorPossibleItem>[] = [
+ { id: "itemCode", label: "아이템 코드", type: "text" },
+ { id: "workType", label: "공종", type: "text" },
+ { id: "itemList", label: "아이템명", type: "text" },
+ { id: "shipTypes", label: "선종", type: "text" },
+ { id: "subItemList", label: "서브아이템", type: "text" },
+ { id: "createdAt", label: "등록일", type: "date" },
+ { id: "updatedAt", label: "수정일", type: "date" },
+ ]
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ // rowAction 상태 변경 감지
+ React.useEffect(() => {
+ if (rowAction?.type === "delete") {
+ setShowDeleteAlert(true)
+ }
+ }, [rowAction])
+
+ return (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <PossibleItemsTableToolbarActions
+ table={table}
+ vendorId={vendorId}
+ onAdd={() => setShowAddDialog(true)}
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ {/* Add Item Dialog */}
+ <AddItemDialog
+ open={showAddDialog}
+ onOpenChange={setShowAddDialog}
+ vendorId={vendorId}
+ />
+
+ {/* Delete Confirmation Dialog */}
+ <AlertDialog open={showDeleteAlert} onOpenChange={setShowDeleteAlert}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>아이템 삭제</AlertDialogTitle>
+ <AlertDialogDescription>
+ 이 아이템을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel onClick={() => setRowAction(null)}>
+ 취소
+ </AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDeleteItem}
+ disabled={isDeleting}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isDeleting ? "삭제 중..." : "삭제"}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ </>
+ )
+}
\ No newline at end of file diff --git a/lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx b/lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx new file mode 100644 index 00000000..707d0513 --- /dev/null +++ b/lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx @@ -0,0 +1,119 @@ +"use client"
+
+import * as React from "react"
+import type { Table } from "@tanstack/react-table"
+import { Plus, Trash2 } from "lucide-react"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+
+import type { TechVendorPossibleItem } from "../validations"
+import { deleteTechVendorPossibleItemsNew } from "../service"
+
+interface PossibleItemsTableToolbarActionsProps {
+ table: Table<TechVendorPossibleItem>
+ vendorId: number
+ onAdd: () => void
+}
+
+export function PossibleItemsTableToolbarActions({
+ table,
+ vendorId,
+ onAdd,
+}: PossibleItemsTableToolbarActionsProps) {
+ const [showDeleteAlert, setShowDeleteAlert] = React.useState(false)
+ const [isDeleting, setIsDeleting] = React.useState(false)
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
+ async function handleDelete() {
+ setIsDeleting(true)
+ try {
+ const ids = selectedRows.map((row) => row.original.id)
+ const { error } = await deleteTechVendorPossibleItemsNew(ids, vendorId)
+
+ if (error) {
+ throw new Error(error)
+ }
+
+ toast.success(`${ids.length}개의 아이템이 삭제되었습니다`)
+ table.resetRowSelection()
+ setShowDeleteAlert(false)
+ } catch {
+ toast.error("아이템 삭제 중 오류가 발생했습니다")
+ } finally {
+ setIsDeleting(false)
+ }
+ }
+
+ return (
+ <>
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={onAdd}
+ >
+ <Plus className="mr-2 h-4 w-4" />
+ 아이템 추가
+ </Button>
+
+ {selectedRows.length > 0 && (
+ <>
+ <Separator orientation="vertical" className="mx-2 h-4" />
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setShowDeleteAlert(true)}
+ disabled={selectedRows.length === 0}
+ >
+ <Trash2 className="mr-2 h-4 w-4" />
+ 삭제 ({selectedRows.length})
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ 선택된 {selectedRows.length}개 아이템을 삭제합니다
+ </TooltipContent>
+ </Tooltip>
+ </>
+ )}
+ </div>
+
+ <AlertDialog open={showDeleteAlert} onOpenChange={setShowDeleteAlert}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>아이템 삭제</AlertDialogTitle>
+ <AlertDialogDescription>
+ 선택된 {selectedRows.length}개의 아이템을 삭제하시겠습니까?
+ 이 작업은 되돌릴 수 없습니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleDelete}
+ disabled={isDeleting}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {isDeleting ? "삭제 중..." : "삭제"}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ </>
+ )
+}
\ No newline at end of file |
