From 50ae0b8f02c034e60d4cbb504620dfa1575a836f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Jul 2025 09:19:42 +0000 Subject: (박서영) 설계 document Numbering Rule 개발-최겸 업로드 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/delete-document-class-dialog.tsx | 154 ++++++++++++++++++ .../table/delete-document-class-option-dialog.tsx | 152 ++++++++++++++++++ .../table/document-class-add-dialog.tsx | 145 +++++++++++++++++ .../table/document-class-edit-sheet.tsx | 160 +++++++++++++++++++ .../table/document-class-option-add-dialog.tsx | 137 ++++++++++++++++ .../table/document-class-option-edit-sheet.tsx | 143 +++++++++++++++++ .../table/document-class-options-table-columns.tsx | 156 ++++++++++++++++++ .../table/document-class-options-table-toolbar.tsx | 43 +++++ .../table/document-class-options-table.tsx | 176 +++++++++++++++++++++ .../table/document-class-table-columns.tsx | 169 ++++++++++++++++++++ .../table/document-class-table-toolbar.tsx | 34 ++++ .../document-class/table/document-class-table.tsx | 107 +++++++++++++ 12 files changed, 1576 insertions(+) create mode 100644 lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx create mode 100644 lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-options-table.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-table-columns.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx create mode 100644 lib/docu-list-rule/document-class/table/document-class-table.tsx (limited to 'lib/docu-list-rule/document-class/table') diff --git a/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx b/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx new file mode 100644 index 00000000..677fe8ef --- /dev/null +++ b/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx @@ -0,0 +1,154 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deleteDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service" +import { documentClasses } from "@/db/schema/documentClasses" + +interface DeleteDocumentClassDialogProps + extends React.ComponentPropsWithoutRef { + documentClasses: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteDocumentClassDialog({ + documentClasses, + showTrigger = true, + onSuccess, + ...props +}: DeleteDocumentClassDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 Document Class를 순차적으로 삭제 + for (const documentClass of documentClasses) { + const result = await deleteDocumentClassCodeGroup(documentClass.id) + if (!result.success) { + toast.error(`Document Class ${documentClass.code} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("Document Class가 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("Document Class 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {documentClasses.length} + 개의 Document Class를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {documentClasses.length} + 개의 Document Class를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx b/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx new file mode 100644 index 00000000..f0fcbc34 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx @@ -0,0 +1,152 @@ +"use client" + +import * as React from "react" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deleteDocumentClassOption } from "@/lib/docu-list-rule/document-class/service" +import { documentClassOptions } from "@/db/schema/documentClasses" + +interface DeleteDocumentClassOptionDialogProps + extends React.ComponentPropsWithoutRef { + options: typeof documentClassOptions.$inferSelect[] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteDocumentClassOptionDialog({ + options, + showTrigger = true, + onSuccess, + ...props +}: DeleteDocumentClassOptionDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + for (const option of options) { + const result = await deleteDocumentClassOption(option.id) + if (!result.success) { + toast.error(`Document Class 옵션 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("Document Class 옵션이 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("Document Class 옵션 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {options.length} + 개의 Document Class 옵션을 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {options.length} + 개의 Document Class 옵션을 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx new file mode 100644 index 00000000..ef9c50a8 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx @@ -0,0 +1,145 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" +import { Plus } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + + +import { createDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service" + +const createDocumentClassSchema = z.object({ + value: z.string().min(1, "Value는 필수입니다."), + description: z.string().optional(), +}) + +type CreateDocumentClassSchema = z.infer + +interface DocumentClassAddDialogProps { + onSuccess?: () => void +} + +export function DocumentClassAddDialog({ + onSuccess, +}: DocumentClassAddDialogProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(createDocumentClassSchema), + defaultValues: { + value: "", + description: "", + }, + mode: "onChange" + }) + + async function onSubmit(input: CreateDocumentClassSchema) { + startTransition(async () => { + try { + const result = await createDocumentClassCodeGroup({ + value: input.value, + description: input.description, + }) + + if (result.success) { + toast.success("Document Class가 생성되었습니다.") + form.reset() + setOpen(false) + onSuccess?.() + } else { + toast.error(result.error || "생성에 실패했습니다.") + } + } catch (error) { + console.error("Create error:", error) + toast.error("Document Class 생성 중 오류가 발생했습니다.") + } + }) + } + + const handleCancel = () => { + form.reset() + setOpen(false) + } + + return ( + + + + + + + Document Class 추가 + + 새로운 Document Class를 추가합니다. + * 표시된 항목은 필수 입력사항입니다. + + +
+ + ( + + Value * + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx new file mode 100644 index 00000000..97729caa --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" +import { Loader } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + +import { updateDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service" +import { documentClasses } from "@/db/schema/documentClasses" + +const updateDocumentClassSchema = z.object({ + value: z.string().min(1, "Value는 필수입니다."), + description: z.string().optional(), +}) + +type UpdateDocumentClassSchema = z.infer + +interface DocumentClassEditSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + data: typeof documentClasses.$inferSelect | null + onSuccess?: () => void +} + +export function DocumentClassEditSheet({ + open, + onOpenChange, + data, + onSuccess, +}: DocumentClassEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(updateDocumentClassSchema), + defaultValues: { + value: data?.value || "", + description: data?.description || "", + }, + mode: "onChange" + }) + + React.useEffect(() => { + if (data) { + form.reset({ + value: data.value || "", + description: data.description || "", + }) + } + }, [data, form]) + + async function onSubmit(input: UpdateDocumentClassSchema) { + if (!data) return + + startUpdateTransition(async () => { + try { + const result = await updateDocumentClassCodeGroup({ + id: data.id, + value: input.value, + description: input.description, + }) + + if (result.success) { + toast.success("Document Class가 성공적으로 수정되었습니다.") + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error || "수정에 실패했습니다.") + } + } catch (error) { + console.error("Update error:", error) + toast.error("Document Class 수정 중 오류가 발생했습니다.") + } + }) + } + + return ( + + + + Document Class 수정 + + Document Class 정보를 수정하고 변경사항을 저장하세요 + + +
+ + ( + + Value + + + + + + )} + /> + ( + + Description + + + + + + )} + /> + + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx new file mode 100644 index 00000000..5bfcbd33 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx @@ -0,0 +1,137 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" +import { Plus } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + +import { createDocumentClassOptionItem } from "@/lib/docu-list-rule/document-class/service" +import { documentClasses } from "@/db/schema/documentClasses" + +const createDocumentClassOptionSchema = z.object({ + optionValue: z.string().min(1, "옵션 값은 필수입니다."), +}) + +type CreateDocumentClassOptionSchema = z.infer + +interface DocumentClassOptionAddDialogProps { + selectedDocumentClass: typeof documentClasses.$inferSelect | null + onSuccess?: () => void +} + +export function DocumentClassOptionAddDialog({ + selectedDocumentClass, + onSuccess, +}: DocumentClassOptionAddDialogProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(createDocumentClassOptionSchema), + defaultValues: { + optionValue: "", + }, + mode: "onChange" + }) + + async function onSubmit(input: CreateDocumentClassOptionSchema) { + if (!selectedDocumentClass) { + toast.error("Document Class가 선택되지 않았습니다.") + return + } + + startTransition(async () => { + try { + const result = await createDocumentClassOptionItem({ + documentClassId: selectedDocumentClass.id, + optionValue: input.optionValue, + }) + + if (result.success) { + toast.success("Document Class 옵션이 생성되었습니다.") + form.reset() + setOpen(false) + onSuccess?.() + } else { + toast.error(result.error || "생성에 실패했습니다.") + } + } catch (error) { + console.error("Create error:", error) + toast.error("Document Class 옵션 생성 중 오류가 발생했습니다.") + } + }) + } + + const handleCancel = () => { + form.reset() + setOpen(false) + } + + return ( + + + + + + + Document Class 옵션 추가 + + {selectedDocumentClass?.description || "Document Class"}에 새로운 옵션을 추가합니다. + * 표시된 항목은 필수 입력사항입니다. + + +
+ + ( + + 옵션 값 * + + + + + + )} + /> + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx new file mode 100644 index 00000000..6f6e7a87 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" +import { Loader } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { updateDocumentClassOption } from "@/lib/docu-list-rule/document-class/service" +import { documentClassOptions } from "@/db/schema/documentClasses" + +const updateDocumentClassOptionSchema = z.object({ + optionValue: z.string().min(1, "옵션 값은 필수입니다."), +}) + +type UpdateDocumentClassOptionSchema = z.infer + +interface DocumentClassOptionEditSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + data: typeof documentClassOptions.$inferSelect | null + onSuccess?: () => void +} + +export function DocumentClassOptionEditSheet({ + open, + onOpenChange, + data, + onSuccess, +}: DocumentClassOptionEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(updateDocumentClassOptionSchema), + defaultValues: { + optionValue: data?.optionValue || "", + }, + mode: "onChange" + }) + + React.useEffect(() => { + if (data) { + form.reset({ + optionValue: data.optionValue || "", + }) + } + }, [data, form]) + + async function onSubmit(input: UpdateDocumentClassOptionSchema) { + if (!data) return + + startUpdateTransition(async () => { + try { + const result = await updateDocumentClassOption({ + id: data.id, + optionValue: input.optionValue, + }) + + if (result.success) { + toast.success("Document Class 옵션이 성공적으로 수정되었습니다.") + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error || "수정에 실패했습니다.") + } + } catch (error) { + console.error("Update error:", error) + toast.error("Document Class 옵션 수정 중 오류가 발생했습니다.") + } + }) + } + + return ( + + + + Document Class 옵션 수정 + + Document Class 옵션 정보를 수정하고 변경사항을 저장하세요 + + +
+ + ( + + 옵션 값 + + + + + + )} + /> + + + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx new file mode 100644 index 00000000..c04a7b37 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx @@ -0,0 +1,156 @@ +"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 { formatDateTime } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { documentClassOptions } from "@/db/schema/documentClasses" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * tanstack table 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef[] = [ + { + accessorKey: "optionCode", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "코드", + type: "text", + }, + cell: ({ row }) => row.getValue("optionCode") ?? "", + minSize: 80 + }, + { + accessorKey: "optionValue", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "옵션 값", + type: "text", + }, + cell: ({ row }) => row.getValue("optionValue") ?? "", + minSize: 80 + }, + + { + accessorKey: "createdAt", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "생성일", + type: "date", + }, + cell: ({ row }) => { + const dateVal = row.getValue("createdAt") as Date + return formatDateTime(dateVal, "KR") + }, + minSize: 80 + } + ] + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx new file mode 100644 index 00000000..5044d90d --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx @@ -0,0 +1,43 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" + +import { DocumentClassOptionAddDialog } from "./document-class-option-add-dialog" +import { DeleteDocumentClassOptionDialog } from "./delete-document-class-option-dialog" +import { documentClasses, documentClassOptions } from "@/db/schema/documentClasses" + +interface DocumentClassOptionsTableToolbarActionsProps { + table: Table + selectedDocumentClass: typeof documentClasses.$inferSelect | null + onSuccess?: () => void +} + +export function DocumentClassOptionsTableToolbarActions({ + table, + selectedDocumentClass, + onSuccess, +}: DocumentClassOptionsTableToolbarActionsProps) { + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedOptions = selectedRows.map((row) => row.original as typeof documentClassOptions.$inferSelect) + + return ( +
+ {/** 선택된 로우가 있으면 삭제 다이얼로그 */} + {selectedOptions.length > 0 ? ( + { + table.toggleAllRowsSelected(false) + onSuccess?.() + }} + /> + ) : null} + + +
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table.tsx new file mode 100644 index 00000000..644e3599 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-options-table.tsx @@ -0,0 +1,176 @@ +"use client" + +import * as React from "react" +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 type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table" + +import { getDocumentClassSubOptions } from "@/lib/docu-list-rule/document-class/service" +import { getColumns } from "./document-class-options-table-columns" +import { DocumentClassOptionEditSheet } from "./document-class-option-edit-sheet" +import { DeleteDocumentClassOptionDialog } from "./delete-document-class-option-dialog" +import { DocumentClassOptionsTableToolbarActions } from "./document-class-options-table-toolbar" +import { documentClasses, documentClassOptions } from "@/db/schema/docu-list-rule" + +type DocumentClass = typeof documentClasses.$inferSelect + +interface DocumentClassOptionsTableProps { + selectedDocumentClass: DocumentClass | null + documentClasses: DocumentClass[] + onSelectDocumentClass: (documentClass: DocumentClass) => void +} + +export function DocumentClassOptionsTable({ + selectedDocumentClass, + documentClasses, + onSelectDocumentClass +}: DocumentClassOptionsTableProps) { + const [rowAction, setRowAction] = React.useState | null>(null) + + // 선택된 Document Class의 옵션 데이터 로드 + const [options, setOptions] = React.useState([]) + + // DB 등록 순서대로 정렬된 Document Classes + const sortedDocumentClasses = React.useMemo(() => { + return [...documentClasses].sort((a, b) => a.id - b.id) + }, [documentClasses]) + + const handleSuccess = React.useCallback(async () => { + // 옵션 테이블 새로고침 + if (selectedDocumentClass) { + try { + const result = await getDocumentClassSubOptions(selectedDocumentClass.id) + if (result.success && result.data) { + setOptions(result.data) + } + } catch (error) { + console.error("Error refreshing options:", error) + } + } + }, [selectedDocumentClass]) + + const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction]) + + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "optionCode", label: "코드", type: "text" }, + { id: "optionValue", label: "옵션 값", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ] + + const { table } = useDataTable({ + data: options, + columns, + pageCount: 1, + enablePinning: true, + enableAdvancedFilter: true, + manualSorting: false, + initialState: { + sorting: [{ id: "id", desc: false }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + React.useEffect(() => { + const loadOptions = async () => { + if (!selectedDocumentClass) { + setOptions([]) + return + } + + try { + const result = await getDocumentClassSubOptions(selectedDocumentClass.id) + if (result.success && result.data) { + setOptions(result.data) + } + } catch (error) { + console.error("Error loading options:", error) + setOptions([]) + } + } + + loadOptions() + }, [selectedDocumentClass]) + + if (!selectedDocumentClass) { + return ( +
+
+ {sortedDocumentClasses.map((documentClass) => ( + + ))} +
+
+ Document Class를 선택하면 옵션을 관리할 수 있습니다. +
+
+ ) + } + + return ( + <> +
+
+ {sortedDocumentClasses.map((documentClass) => ( + + ))} +
+ + + + + + +
+ + setRowAction(null)} + options={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + rowAction?.row.toggleSelected(false) + handleSuccess() + }} + /> + + setRowAction(null)} + data={rowAction?.row.original ?? null} + onSuccess={handleSuccess} + /> + + ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx new file mode 100644 index 00000000..6684d13a --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx @@ -0,0 +1,169 @@ +"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 { formatDateTime } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { documentClasses } from "@/db/schema/docu-list-rule" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * tanstack table 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef[] = [ + { + accessorKey: "code", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "코드", + type: "text", + }, + cell: ({ row }) => row.getValue("code") ?? "", + minSize: 80 + }, + { + accessorKey: "value", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "값", + type: "text", + }, + cell: ({ row }) => row.getValue("value") ?? "", + minSize: 80 + }, + { + accessorKey: "description", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "설명", + type: "text", + }, + cell: ({ row }) => row.getValue("description") ?? "", + minSize: 80 + }, + + { + accessorKey: "createdAt", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "생성일", + type: "date", + }, + cell: ({ row }) => { + const dateVal = row.getValue("createdAt") as Date + return formatDateTime(dateVal, "KR") + }, + minSize: 80 + } + ] + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx b/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx new file mode 100644 index 00000000..7bc28a06 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx @@ -0,0 +1,34 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" + +import { DeleteDocumentClassDialog } from "./delete-document-class-dialog" +import { DocumentClassAddDialog } from "./document-class-add-dialog" +import { documentClasses } from "@/db/schema/documentClasses" + +interface DocumentClassTableToolbarActionsProps { + table: Table + onSuccess?: () => void +} + +export function DocumentClassTableToolbarActions({ table, onSuccess }: DocumentClassTableToolbarActionsProps) { + return ( +
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + onSuccess={() => { + table.toggleAllRowsSelected(false) + onSuccess?.() + }} + /> + ) : null} + + +
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/document-class/table/document-class-table.tsx b/lib/docu-list-rule/document-class/table/document-class-table.tsx new file mode 100644 index 00000000..bbe79800 --- /dev/null +++ b/lib/docu-list-rule/document-class/table/document-class-table.tsx @@ -0,0 +1,107 @@ +"use client" + +import * as React from "react" +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 type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table" +import { getDocumentClassCodeGroups } from "@/lib/docu-list-rule/document-class/service" +import { getColumns } from "./document-class-table-columns" +import { DocumentClassEditSheet } from "./document-class-edit-sheet" +import { DocumentClassOptionsTable } from "./document-class-options-table" +import { DocumentClassTableToolbarActions } from "./document-class-table-toolbar" +import { DeleteDocumentClassDialog } from "./delete-document-class-dialog" +import { documentClasses } from "@/db/schema/docu-list-rule" + +interface DocumentClassTableProps { + promises?: Promise<[{ data: typeof documentClasses.$inferSelect[]; pageCount: number }]> +} + +export function DocumentClassTable({ promises }: DocumentClassTableProps) { + const rawData = React.use(promises!) + const [rowAction, setRowAction] = React.useState | null>(null) + const [selectedDocumentClass, setSelectedDocumentClass] = React.useState(null) + + const refreshData = React.useCallback(() => { + window.location.reload() + }, []) + + const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction]) + + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "code", label: "코드", type: "text" }, + { id: "value", label: "값", type: "text" }, + { id: "description", label: "설명", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ] + + const { table } = useDataTable({ + data: rawData[0].data as any, + columns, + pageCount: rawData[0].pageCount, + enablePinning: true, + enableAdvancedFilter: true, + manualSorting: false, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + + + + + + + {/* 구분선 */} +
+ + {/* Document Class 옵션 관리 제목 */} +
+
+
+

Document Class 옵션 관리

+
+

+ Document Class 옵션들을 관리합니다. +

+
+
+ + {/* Document Class 옵션 테이블 */} + + + setRowAction(null)} + documentClasses={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + rowAction?.row.toggleSelected(false) + refreshData() + }} + /> + + setRowAction(null)} + data={rowAction?.row.original ?? null} + onSuccess={refreshData} + /> + + ) +} \ No newline at end of file -- cgit v1.2.3