diff options
Diffstat (limited to 'lib/tech-vendors/possible-items')
4 files changed, 651 insertions, 508 deletions
diff --git a/lib/tech-vendors/possible-items/add-item-dialog.tsx b/lib/tech-vendors/possible-items/add-item-dialog.tsx index ef15a5ce..0e6edd19 100644 --- a/lib/tech-vendors/possible-items/add-item-dialog.tsx +++ b/lib/tech-vendors/possible-items/add-item-dialog.tsx @@ -27,6 +27,7 @@ interface ItemData { workType: string | null;
shipTypes?: string | null;
subItemList?: string | null;
+ itemType: "SHIP" | "TOP" | "HULL";
createdAt: Date;
updatedAt: Date;
}
@@ -80,7 +81,7 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro console.log("Loaded items:", result.data.length, result.data);
// itemCode가 null이 아닌 항목만 필터링
const validItems = result.data.filter(item => item.itemCode != null);
- setItems(validItems);
+ setItems(validItems as ItemData[]);
} catch (error) {
console.error("Failed to load items:", error);
toast.error("아이템 목록을 불러오는데 실패했습니다.");
@@ -93,13 +94,13 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro if (!item.itemCode) return; // itemCode가 null인 경우 처리하지 않음
setSelectedItems(prev => {
- // itemCode + shipTypes 조합으로 중복 체크
+ // id + itemType 조합으로 중복 체크
const isSelected = prev.some(i =>
- i.itemCode === item.itemCode && i.shipTypes === item.shipTypes
+ i.id === item.id && i.itemType === item.itemType
);
if (isSelected) {
return prev.filter(i =>
- !(i.itemCode === item.itemCode && i.shipTypes === item.shipTypes)
+ !(i.id === item.id && i.itemType === item.itemType)
);
} else {
return [...prev, item];
@@ -120,11 +121,8 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro 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,
+ itemId: item.id,
+ itemType: item.itemType,
});
if (result.success) {
@@ -197,10 +195,11 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro <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}` : ''}`;
+ const itemKey = `${item.itemType}-${item.id}-${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
+ const displayText = `[${item.itemType}] ${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
return (
<Badge key={`selected-${itemKey}`} variant="default" className="text-xs">
- {itemKey}
+ {displayText}
<X
className="ml-1 h-3 w-3 cursor-pointer"
onClick={(e) => {
@@ -232,11 +231,11 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro filteredItems.map((item) => {
if (!item.itemCode) return null; // itemCode가 null인 경우 렌더링하지 않음
- // itemCode + shipTypes 조합으로 선택 여부 체크
+ // id + itemType 조합으로 선택 여부 체크
const isSelected = selectedItems.some(i =>
- i.itemCode === item.itemCode && i.shipTypes === item.shipTypes
+ i.id === item.id && i.itemType === item.itemType
);
- const itemKey = `${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
+ const itemKey = `${item.itemType}-${item.id}-${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`;
return (
<div
@@ -249,7 +248,7 @@ export function AddItemDialog({ open, onOpenChange, vendorId }: AddItemDialogPro onClick={() => handleItemToggle(item)}
>
<div className="font-medium">
- {itemKey}
+ {`[${item.itemType}] ${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ''}`}
</div>
<div className="text-sm text-muted-foreground">
{item.itemList || "-"}
diff --git a/lib/tech-vendors/possible-items/possible-items-columns.tsx b/lib/tech-vendors/possible-items/possible-items-columns.tsx index ef48c5b5..252dae9b 100644 --- a/lib/tech-vendors/possible-items/possible-items-columns.tsx +++ b/lib/tech-vendors/possible-items/possible-items-columns.tsx @@ -1,206 +1,266 @@ -"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,
- },
- ]
+"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 }) => { + const itemCode = row.getValue("itemCode") as string | undefined + return ( + <div className="font-mono text-sm"> + {itemCode || <span className="text-muted-foreground">-</span>} + </div> + ) + }, + size: 150, + }, + + // // 타입 + // { + // accessorKey: "techVendorType", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="타입" /> + // ), + // cell: ({ row }) => { + // const techVendorType = row.getValue("techVendorType") as string | undefined + + // // 벤더 타입 파싱 개선 - null/undefined 안전 처리 + // let types: string[] = []; + // if (!techVendorType) { + // types = []; + // } else if (techVendorType.startsWith('[') && techVendorType.endsWith(']')) { + // // JSON 배열 형태 + // try { + // const parsed = JSON.parse(techVendorType); + // types = Array.isArray(parsed) ? parsed.filter(Boolean) : [techVendorType]; + // } catch { + // types = [techVendorType]; + // } + // } else if (techVendorType.includes(',')) { + // // 콤마로 구분된 문자열 + // types = techVendorType.split(',').map(t => t.trim()).filter(Boolean); + // } else { + // // 단일 문자열 + // types = [techVendorType.trim()].filter(Boolean); + // } + + // // 벤더 타입 정렬 - 조선 > 해양TOP > 해양HULL 순 + // const typeOrder = ["조선", "해양TOP", "해양HULL"]; + // types.sort((a, b) => { + // const indexA = typeOrder.indexOf(a); + // const indexB = typeOrder.indexOf(b); + + // // 정의된 순서에 있는 경우 우선순위 적용 + // if (indexA !== -1 && indexB !== -1) { + // return indexA - indexB; + // } + // return a.localeCompare(b); + // }); + + // return ( + // <div className="flex flex-wrap gap-1"> + // {types.length > 0 ? types.map((type, index) => ( + // <Badge key={`${type}-${index}`} variant="secondary" className="text-xs"> + // {type} + // </Badge> + // )) : ( + // <span className="text-muted-foreground">-</span> + // )} + // </div> + // ) + // }, + // size: 120, + // }, + + // 공종 + { + 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, "ko-KR")} + </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, "ko-KR")} + </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 index 9c024a93..b54e12d4 100644 --- a/lib/tech-vendors/possible-items/possible-items-table.tsx +++ b/lib/tech-vendors/possible-items/possible-items-table.tsx @@ -1,171 +1,256 @@ -"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>
- </>
- )
+"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 { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" + +import { getColumns } from "./possible-items-columns" +import { getTechVendorPossibleItems } from "../../tech-vendor-possible-items/service" +import { deleteTechVendorPossibleItem, getTechVendorDetailById } 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) + + // vendor 정보와 items 다이얼로그 관련 상태 + const [vendorInfo, setVendorInfo] = React.useState<{ + id: number + vendorName: string + status: string + items: string | null + } | null>(null) + const [showItemsDialog, setShowItemsDialog] = React.useState(false) + const [isLoadingVendor, setIsLoadingVendor] = React.useState(false) + + // getColumns() 호출 시, setRowAction을 주입 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // vendor 정보 가져오기 + React.useEffect(() => { + async function fetchVendorInfo() { + try { + setIsLoadingVendor(true) + const vendorData = await getTechVendorDetailById(vendorId) + setVendorInfo(vendorData) + } catch (error) { + console.error("Error fetching vendor info:", error) + toast.error("벤더 정보를 불러오는데 실패했습니다") + } finally { + setIsLoadingVendor(false) + } + } + + fetchVendorInfo() + }, [vendorId]) + + // 단일 아이템 삭제 핸들러 + 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: "vendorId", label: "벤더 ID" }, + { id: "techVendorType", label: "아이템 타입" }, + ] + + const advancedFilterFields: DataTableAdvancedFilterField<TechVendorPossibleItem>[] = [ + { id: "vendorId", label: "벤더 ID", type: "number" }, + { 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: "techVendorType", label: "아이템 타입", type: "text" }, + { id: "createdAt", label: "등록일", type: "date" }, + { id: "updatedAt", label: "수정일", type: "date" }, + ] + + const { table } = useDataTable({ + data: data as TechVendorPossibleItem[], // 타입 단언 추가 + 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]) + + // 견적비교용 벤더인지 확인 + const isQuoteComparisonVendor = vendorInfo?.status === "QUOTE_COMPARISON" + + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <div className="flex items-center gap-2"> + {/* 견적비교용 벤더일 때만 items 버튼 표시 */} + {isQuoteComparisonVendor && ( + <Button + variant="outline" + size="sm" + onClick={() => setShowItemsDialog(true)} + disabled={isLoadingVendor} + className="flex items-center gap-2" + > + <Badge variant="secondary" className="text-xs"> + 수기입력된 자재 항목 + </Badge> + </Button> + )} + + <PossibleItemsTableToolbarActions + table={table} + vendorId={vendorId} + onAdd={() => setShowAddDialog(true)} // 주석처리 + /> + </div> + </DataTableAdvancedToolbar> + </DataTable> + + {/* Add Item Dialog */} + <AddItemDialog + open={showAddDialog} + onOpenChange={setShowAddDialog} + vendorId={vendorId} + /> + + {/* Vendor Items Dialog */} + <Dialog open={showItemsDialog} onOpenChange={setShowItemsDialog}> + <DialogContent className="max-w-2xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>벤더 수기입력 자재 항목</DialogTitle> + <DialogDescription> + {vendorInfo?.vendorName} 벤더가 직접 입력한 자재 항목입니다. + </DialogDescription> + </DialogHeader> + + <ScrollArea className="max-h-[60vh]"> + {vendorInfo?.items && vendorInfo.items.length > 0 ? ( + <div className="space-y-3"> + <p className="text-sm text-muted-foreground mt-1"> + {vendorInfo.items} + </p> + </div> + ) : ( + <div className="text-center py-8 text-muted-foreground"> + <p>등록된 수기입력 자재 항목이 없습니다.</p> + </div> + )} + </ScrollArea> + </DialogContent> + </Dialog> + + {/* 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 index 074dc187..192bf614 100644 --- a/lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx +++ b/lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx @@ -1,119 +1,118 @@ -"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>
- </>
- )
+"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 { 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 && ( + <> + <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 |
