summaryrefslogtreecommitdiff
path: root/lib/contact-possible-items/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/contact-possible-items/table')
-rw-r--r--lib/contact-possible-items/table/contact-possible-items-table-columns.tsx301
-rw-r--r--lib/contact-possible-items/table/contact-possible-items-table-toolbar-actions.tsx92
-rw-r--r--lib/contact-possible-items/table/contact-possible-items-table.tsx102
-rw-r--r--lib/contact-possible-items/table/delete-contact-possible-items-dialog.tsx111
4 files changed, 606 insertions, 0 deletions
diff --git a/lib/contact-possible-items/table/contact-possible-items-table-columns.tsx b/lib/contact-possible-items/table/contact-possible-items-table-columns.tsx
new file mode 100644
index 00000000..a3b198ae
--- /dev/null
+++ b/lib/contact-possible-items/table/contact-possible-items-table-columns.tsx
@@ -0,0 +1,301 @@
+"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 { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import { ContactPossibleItemDetail } from "../service"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ContactPossibleItemDetail> | null>>;
+}
+
+/**
+ * tanstack table 컬럼 정의 (중첩 헤더 버전)
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ContactPossibleItemDetail>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<ContactPossibleItemDetail> = {
+ 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,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<ContactPossibleItemDetail> = {
+ 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,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
+ // ----------------------------------------------------------------
+ const baseColumns: ColumnDef<ContactPossibleItemDetail>[] = [
+ // 벤더 정보
+ {
+ id: "vendorInfo",
+ header: "벤더 정보",
+ columns: [
+ {
+ accessorKey: "vendorCode",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더 코드" />
+ ),
+ cell: ({ row }) => row.original.vendorCode ?? "",
+ },
+ {
+ accessorKey: "vendorName",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더명" />
+ ),
+ cell: ({ row }) => row.original.vendorName ?? "",
+ },
+ {
+ accessorKey: "vendorCountry",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더 국가" />
+ ),
+ cell: ({ row }) => {
+ const country = row.original.vendorCountry
+ return country || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "techVendorType",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="벤더 타입" />
+ ),
+ cell: ({ row }) => {
+ const type = row.original.techVendorType
+ return type || <span className="text-muted-foreground">-</span>
+ },
+ },
+ ]
+ },
+ // 담당자 정보
+ {
+ id: "contactInfo",
+ header: "담당자 정보",
+ columns: [
+ {
+ accessorKey: "contactName",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당자명" />
+ ),
+ cell: ({ row }) => {
+ const contactName = row.original.contactName
+ return contactName || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "contactPosition",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="직책" />
+ ),
+ cell: ({ row }) => {
+ const position = row.original.contactPosition
+ return position || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "contactEmail",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당자 이메일" />
+ ),
+ cell: ({ row }) => {
+ const contactEmail = row.original.contactEmail
+ return contactEmail || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "contactPhone",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당자 전화번호" />
+ ),
+ cell: ({ row }) => {
+ const contactPhone = row.original.contactPhone
+ return contactPhone || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "contactCountry",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당자 국가" />
+ ),
+ cell: ({ row }) => {
+ const contactCountry = row.original.contactCountry
+ return contactCountry || <span className="text-muted-foreground">-</span>
+ },
+ },
+ {
+ accessorKey: "isPrimary",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="주담당자" />
+ ),
+ cell: ({ row }) => {
+ const isPrimary = row.original.isPrimary
+ return isPrimary ? "예" : "아니오"
+ },
+ },
+ ]
+ },
+ // 아이템 정보
+ {
+ id: "itemInfo",
+ header: "아이템 정보",
+ columns: [
+ {
+ accessorKey: "itemCode",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="아이템 코드" />
+ ),
+ cell: ({ row }) => row.original.itemCode ?? "",
+ },
+ {
+ accessorKey: "itemList",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="아이템 리스트" />
+ ),
+ cell: ({ row }) => row.original.itemList ?? "",
+ },
+ {
+ accessorKey: "workType",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="공종" />
+ ),
+ cell: ({ row }) => row.original.workType ?? "",
+ },
+ {
+ accessorKey: "shipTypes",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="선종" />
+ ),
+ cell: ({ row }) => row.original.shipTypes ?? "",
+ },
+ {
+ accessorKey: "subItemList",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="서브아이템 리스트" />
+ ),
+ cell: ({ row }) => row.original.subItemList ?? "",
+ },
+ ]
+ },
+
+ // 시스템 정보
+ {
+ id: "systemInfo",
+ header: "시스템 정보",
+ columns: [
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ cell: ({ row }) => {
+ const dateVal = row.getValue("createdAt") as Date
+ return formatDate(dateVal)
+ },
+ },
+ {
+ accessorKey: "updatedAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="수정일" />
+ ),
+ cell: ({ row }) => {
+ const dateVal = row.getValue("updatedAt") as Date
+ return formatDate(dateVal)
+ },
+ },
+ ]
+ },
+ ]
+
+ // ----------------------------------------------------------------
+ // 4) 최종 컬럼 배열: select, baseColumns, actions
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ ...baseColumns,
+ actionsColumn,
+ ]
+} \ No newline at end of file
diff --git a/lib/contact-possible-items/table/contact-possible-items-table-toolbar-actions.tsx b/lib/contact-possible-items/table/contact-possible-items-table-toolbar-actions.tsx
new file mode 100644
index 00000000..4125399b
--- /dev/null
+++ b/lib/contact-possible-items/table/contact-possible-items-table-toolbar-actions.tsx
@@ -0,0 +1,92 @@
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { Download } from "lucide-react"
+import * as ExcelJS from 'exceljs'
+
+import { Button } from "@/components/ui/button"
+import { ContactPossibleItemDetail } from "../service"
+
+interface ContactPossibleItemsTableToolbarActionsProps {
+ table: Table<ContactPossibleItemDetail>
+}
+
+export function ContactPossibleItemsTableToolbarActions({
+ table,
+}: ContactPossibleItemsTableToolbarActionsProps) {
+
+ const handleExport = () => {
+ // 현재 테이블의 모든 데이터를 Excel로 내보내기
+ const data = table.getFilteredRowModel().rows.map(row => ({
+ "벤더 코드": row.original.vendorCode,
+ "벤더명": row.original.vendorName,
+ "벤더 국가": row.original.vendorCountry,
+ "벤더 타입": row.original.techVendorType,
+ "담당자명": row.original.contactName,
+ "담당자 직책": row.original.contactPosition,
+ "담당자 이메일": row.original.contactEmail,
+ "담당자 전화번호": row.original.contactPhone,
+ "담당자 국가": row.original.contactCountry,
+ "주담당자": row.original.isPrimary ? "예" : "아니오",
+ "아이템 코드": row.original.itemCode,
+ "아이템명": row.original.itemList,
+ "공종": row.original.workType,
+ "선종": row.original.shipTypes,
+ "서브아이템": row.original.subItemList,
+ "생성일": new Date(row.original.createdAt).toLocaleDateString("ko-KR"),
+ "수정일": new Date(row.original.updatedAt).toLocaleDateString("ko-KR"),
+ }))
+
+ downloadExcel(data, "contact_possible_items.xlsx")
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleExport}
+ >
+ <Download className="mr-2 h-4 w-4" />
+ Excel 내보내기
+ </Button>
+ </div>
+ )
+}
+
+// Excel 파일 다운로드
+function downloadExcel(data: Array<Record<string, unknown>>, filename: string) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("Data")
+
+ if (data.length > 0) {
+ // 헤더 추가
+ const headers = Object.keys(data[0])
+ worksheet.addRow(headers)
+
+ // 데이터 추가
+ data.forEach(row => {
+ worksheet.addRow(Object.values(row))
+ })
+
+ // 스타일 적용
+ worksheet.getRow(1).font = { bold: true }
+ worksheet.columns.forEach(column => {
+ column.width = 15
+ })
+ }
+
+ // 파일 다운로드
+ workbook.xlsx.writeBuffer().then(buffer => {
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ })
+ const url = window.URL.createObjectURL(blob)
+ const a = document.createElement("a")
+ a.href = url
+ a.download = filename
+ a.click()
+ window.URL.revokeObjectURL(url)
+ })
+} \ No newline at end of file
diff --git a/lib/contact-possible-items/table/contact-possible-items-table.tsx b/lib/contact-possible-items/table/contact-possible-items-table.tsx
new file mode 100644
index 00000000..3828e26c
--- /dev/null
+++ b/lib/contact-possible-items/table/contact-possible-items-table.tsx
@@ -0,0 +1,102 @@
+"use client"
+
+import React from "react"
+import { DataTable } from "@/components/data-table/data-table"
+import { ContactPossibleItemsTableToolbarActions } from "./contact-possible-items-table-toolbar-actions"
+import { getColumns } from "./contact-possible-items-table-columns"
+import { ContactPossibleItemDetail } from "../service"
+import { DeleteContactPossibleItemsDialog } from "./delete-contact-possible-items-dialog"
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { type DataTableAdvancedFilterField } from "@/types/table"
+
+// 필터 필드 정의
+const advancedFilterFields: DataTableAdvancedFilterField<ContactPossibleItemDetail>[] = [
+ {
+ id: "contactName",
+ label: "담당자명",
+ type: "text",
+ placeholder: "담당자명으로 검색...",
+ },
+ {
+ id: "vendorName",
+ label: "벤더명",
+ type: "text",
+ placeholder: "벤더명으로 검색...",
+ },
+ {
+ id: "vendorCode",
+ label: "벤더코드",
+ type: "text",
+ placeholder: "벤더코드로 검색...",
+ },
+ {
+ id: "itemCode",
+ label: "아이템코드",
+ type: "text",
+ placeholder: "아이템코드로 검색...",
+ },
+ {
+ id: "workType",
+ label: "공종",
+ type: "text",
+ placeholder: "공종으로 검색...",
+ },
+]
+
+interface ContactPossibleItemsTableProps {
+ contactPossibleItemsPromise: Promise<{
+ data: ContactPossibleItemDetail[]
+ pageCount: number
+ total: number
+ }>
+}
+
+export function ContactPossibleItemsTable({
+ contactPossibleItemsPromise,
+}: ContactPossibleItemsTableProps) {
+ const { data, pageCount, total } = React.use(contactPossibleItemsPromise)
+
+ const [rowAction, setRowAction] = React.useState<any | null>(null)
+
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ rowCount: total,
+ })
+
+ return (
+ <div className="w-full space-y-2.5 overflow-auto">
+ {/* 메인 테이블 */}
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <ContactPossibleItemsTableToolbarActions
+ table={table}
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ <DeleteContactPossibleItemsDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ contactPossibleItems={
+ rowAction?.type === "delete" && rowAction.row
+ ? [rowAction.row.original]
+ : []
+ }
+ showTrigger={false}
+ onSuccess={() => setRowAction(null)}
+ />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/lib/contact-possible-items/table/delete-contact-possible-items-dialog.tsx b/lib/contact-possible-items/table/delete-contact-possible-items-dialog.tsx
new file mode 100644
index 00000000..7c2fc459
--- /dev/null
+++ b/lib/contact-possible-items/table/delete-contact-possible-items-dialog.tsx
@@ -0,0 +1,111 @@
+"use client"
+
+import * as React from "react"
+import { Loader2, Trash2Icon } from "lucide-react"
+
+import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog"
+import { Button } from "@/components/ui/button"
+import { toast } from "@/hooks/use-toast"
+
+import { deleteContactPossibleItems, type ContactPossibleItemDetail } from "../service"
+
+interface DeleteContactPossibleItemsDialogProps {
+ contactPossibleItems: ContactPossibleItemDetail[]
+ showTrigger?: boolean
+ trigger?: React.ReactNode
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ onSuccess?: () => void
+}
+
+export function DeleteContactPossibleItemsDialog({
+ contactPossibleItems,
+ showTrigger = true,
+ trigger,
+ open,
+ onOpenChange,
+ onSuccess,
+}: DeleteContactPossibleItemsDialogProps) {
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ try {
+ const ids = contactPossibleItems.map((item) => item.id)
+ const result = await deleteContactPossibleItems(ids)
+
+ if (result.success) {
+ toast({
+ title: "성공",
+ description: `${contactPossibleItems.length}개의 담당자별 아이템이 삭제되었습니다.`,
+ })
+ onSuccess?.()
+ onOpenChange?.(false)
+ } else {
+ toast({
+ title: "오류",
+ description: result.error || "담당자별 아이템 삭제에 실패했습니다.",
+ variant: "destructive",
+ })
+ }
+ } catch {
+ toast({
+ title: "오류",
+ description: "담당자별 아이템 삭제 중 오류가 발생했습니다.",
+ variant: "destructive",
+ })
+ }
+ })
+ }
+
+ const isMultiple = contactPossibleItems.length > 1
+
+ return (
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
+ {showTrigger && (
+ <AlertDialogTrigger asChild>
+ {trigger ?? (
+ <Button variant="outline" size="sm">
+ <Trash2Icon className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({contactPossibleItems.length})
+ </Button>
+ )}
+ </AlertDialogTrigger>
+ )}
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>
+ {isMultiple
+ ? `${contactPossibleItems.length}개의 담당자별 아이템을 삭제하시겠습니까?`
+ : "담당자별 아이템을 삭제하시겠습니까?"}
+ </AlertDialogTitle>
+ <AlertDialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택한 담당자별 아이템{isMultiple ? "들이" : "이"} 영구적으로 삭제됩니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isDeletePending}>취소</AlertDialogCancel>
+ <Button
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ 삭제
+ </Button>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+} \ No newline at end of file