summaryrefslogtreecommitdiff
path: root/lib/tech-vendors/possible-items
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors/possible-items')
-rw-r--r--lib/tech-vendors/possible-items/add-item-dialog.tsx29
-rw-r--r--lib/tech-vendors/possible-items/possible-items-columns.tsx470
-rw-r--r--lib/tech-vendors/possible-items/possible-items-table.tsx425
-rw-r--r--lib/tech-vendors/possible-items/possible-items-toolbar-actions.tsx235
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