summaryrefslogtreecommitdiff
path: root/lib/docu-list-rule/document-class/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-28 09:19:42 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-28 09:19:42 +0000
commit50ae0b8f02c034e60d4cbb504620dfa1575a836f (patch)
tree24c661a0c7354e15ad56e2bded4d300bd7fd2b41 /lib/docu-list-rule/document-class/table
parent738f956aa61264ffa761e30398eca23393929f8c (diff)
(박서영) 설계 document Numbering Rule 개발-최겸 업로드
Diffstat (limited to 'lib/docu-list-rule/document-class/table')
-rw-r--r--lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx154
-rw-r--r--lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx152
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx145
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx160
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx137
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx143
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx156
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx43
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table.tsx176
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-columns.tsx169
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx34
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table.tsx107
12 files changed, 1576 insertions, 0 deletions
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<typeof Dialog> {
+ documentClasses: Row<typeof documentClasses.$inferSelect>["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 (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({documentClasses.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{documentClasses.length}</span>
+ 개의 Document Class를 서버에서 영구적으로 삭제합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({documentClasses.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{documentClasses.length}</span>
+ 개의 Document Class를 서버에서 영구적으로 삭제합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ 삭제
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ 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<typeof Dialog> {
+ 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 (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({options.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{options.length}</span>
+ 개의 Document Class 옵션을 서버에서 영구적으로 삭제합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ 삭제 ({options.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{options.length}</span>
+ 개의 Document Class 옵션을 서버에서 영구적으로 삭제합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button
+ aria-label="선택된 행 삭제"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ 삭제
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ 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<typeof createDocumentClassSchema>
+
+interface DocumentClassAddDialogProps {
+ onSuccess?: () => void
+}
+
+export function DocumentClassAddDialog({
+ onSuccess,
+}: DocumentClassAddDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [isPending, startTransition] = React.useTransition()
+
+ const form = useForm<CreateDocumentClassSchema>({
+ 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 (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Plus className="mr-2 h-4 w-4" />
+ Add
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Document Class 추가</DialogTitle>
+ <DialogDescription>
+ 새로운 Document Class를 추가합니다.
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+ </DialogDescription>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="value"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Value *</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="예: A Class" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="description"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Description</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="예: A Class Description (선택사항)" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <DialogFooter>
+ <Button type="button" variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ <Button type="submit" disabled={isPending || !form.formState.isValid}>
+ {isPending ? "추가 중..." : "추가"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ 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<typeof updateDocumentClassSchema>
+
+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<UpdateDocumentClassSchema>({
+ 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 (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-md">
+ <SheetHeader className="text-left">
+ <SheetTitle>Document Class 수정</SheetTitle>
+ <SheetDescription>
+ Document Class 정보를 수정하고 변경사항을 저장하세요
+ </SheetDescription>
+ </SheetHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ <FormField
+ control={form.control}
+ name="value"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Value</FormLabel>
+ <FormControl>
+ <Input placeholder="예: A Class" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="description"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Description</FormLabel>
+ <FormControl>
+ <Input placeholder="예: Document Class_1 (선택사항)" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ type="submit"
+ disabled={isUpdatePending || !form.formState.isValid}
+ >
+ {isUpdatePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 저장
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+} \ 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<typeof createDocumentClassOptionSchema>
+
+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<CreateDocumentClassOptionSchema>({
+ 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 (
+ <Dialog open={open && !!selectedDocumentClass} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" disabled={!selectedDocumentClass}>
+ <Plus className="mr-2 h-4 w-4" />
+ 옵션 추가
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Document Class 옵션 추가</DialogTitle>
+ <DialogDescription>
+ {selectedDocumentClass?.description || "Document Class"}에 새로운 옵션을 추가합니다.
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+ </DialogDescription>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="optionValue"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>옵션 값 *</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="옵션 값을 입력하세요" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <DialogFooter>
+ <Button type="button" variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ <Button type="submit" disabled={isPending || !form.formState.isValid}>
+ {isPending ? "추가 중..." : "추가"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ 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<typeof updateDocumentClassOptionSchema>
+
+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<UpdateDocumentClassOptionSchema>({
+ 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 (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-md">
+ <SheetHeader className="text-left">
+ <SheetTitle>Document Class 옵션 수정</SheetTitle>
+ <SheetDescription>
+ Document Class 옵션 정보를 수정하고 변경사항을 저장하세요
+ </SheetDescription>
+ </SheetHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ <FormField
+ control={form.control}
+ name="optionValue"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>옵션 값</FormLabel>
+ <FormControl>
+ <Input placeholder="옵션 값을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ type="submit"
+ disabled={isUpdatePending || !form.formState.isValid}
+ >
+ {isUpdatePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 저장
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+} \ 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<React.SetStateAction<DataTableRowAction<typeof documentClassOptions.$inferSelect> | null>>
+}
+
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof documentClassOptions.$inferSelect>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<typeof documentClassOptions.$inferSelect> = {
+ 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"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<typeof documentClassOptions.$inferSelect> = {
+ 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: "update" })}
+ >
+ Edit
+ </DropdownMenuItem>
+
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "delete" })}
+ >
+ Delete
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<typeof documentClassOptions.$inferSelect>[] = [
+ {
+ accessorKey: "optionCode",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="코드" />
+ ),
+ meta: {
+ excelHeader: "코드",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("optionCode") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "optionValue",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="옵션 값" />
+ ),
+ meta: {
+ excelHeader: "옵션 값",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("optionValue") ?? "",
+ minSize: 80
+ },
+
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ 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<TData> {
+ table: Table<TData>
+ selectedDocumentClass: typeof documentClasses.$inferSelect | null
+ onSuccess?: () => void
+}
+
+export function DocumentClassOptionsTableToolbarActions<TData>({
+ table,
+ selectedDocumentClass,
+ onSuccess,
+}: DocumentClassOptionsTableToolbarActionsProps<TData>) {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ const selectedOptions = selectedRows.map((row) => row.original as typeof documentClassOptions.$inferSelect)
+
+ return (
+ <div className="flex items-center gap-2">
+ {/** 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {selectedOptions.length > 0 ? (
+ <DeleteDocumentClassOptionDialog
+ options={selectedOptions}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false)
+ onSuccess?.()
+ }}
+ />
+ ) : null}
+
+ <DocumentClassOptionAddDialog
+ selectedDocumentClass={selectedDocumentClass}
+ onSuccess={onSuccess}
+ />
+ </div>
+ )
+} \ 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<DataTableRowAction<typeof documentClassOptions.$inferSelect> | null>(null)
+
+ // 선택된 Document Class의 옵션 데이터 로드
+ const [options, setOptions] = React.useState<typeof documentClassOptions.$inferSelect[]>([])
+
+ // 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<typeof documentClassOptions.$inferSelect>[] = [
+ { 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 (
+ <div className="space-y-4">
+ <div className="flex gap-2">
+ {sortedDocumentClasses.map((documentClass) => (
+ <button
+ key={documentClass.id}
+ onClick={() => onSelectDocumentClass(documentClass)}
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ selectedDocumentClass?.id === documentClass.id
+ ? "bg-primary text-primary-foreground"
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
+ }`}
+ >
+ {documentClass.value}
+ </button>
+ ))}
+ </div>
+ <div className="text-center text-muted-foreground py-4">
+ Document Class를 선택하면 옵션을 관리할 수 있습니다.
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <>
+ <div className="space-y-2">
+ <div className="flex gap-2">
+ {sortedDocumentClasses.map((documentClass) => (
+ <button
+ key={documentClass.id}
+ onClick={() => onSelectDocumentClass(documentClass)}
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ selectedDocumentClass?.id === documentClass.id
+ ? "bg-primary text-primary-foreground"
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
+ }`}
+ >
+ {documentClass.value}
+ </button>
+ ))}
+ </div>
+
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <DocumentClassOptionsTableToolbarActions
+ table={table}
+ selectedDocumentClass={selectedDocumentClass}
+ onSuccess={handleSuccess}
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </div>
+
+ <DeleteDocumentClassOptionDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ options={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ handleSuccess()
+ }}
+ />
+
+ <DocumentClassOptionEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => 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<React.SetStateAction<DataTableRowAction<typeof documentClasses.$inferSelect> | null>>
+}
+
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof documentClasses.$inferSelect>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<typeof documentClasses.$inferSelect> = {
+ 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"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<typeof documentClasses.$inferSelect> = {
+ 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: "update" })}
+ >
+ Edit
+ </DropdownMenuItem>
+
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "delete" })}
+ >
+ Delete
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<typeof documentClasses.$inferSelect>[] = [
+ {
+ accessorKey: "code",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="코드" />
+ ),
+ meta: {
+ excelHeader: "코드",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("code") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "value",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="값" />
+ ),
+ meta: {
+ excelHeader: "값",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("value") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "description",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설명" />
+ ),
+ meta: {
+ excelHeader: "설명",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("description") ?? "",
+ minSize: 80
+ },
+
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ 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<typeof documentClasses.$inferSelect>
+ onSuccess?: () => void
+}
+
+export function DocumentClassTableToolbarActions({ table, onSuccess }: DocumentClassTableToolbarActionsProps) {
+ return (
+ <div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteDocumentClassDialog
+ documentClasses={table
+ .getFilteredSelectedRowModel()
+ .rows.map((row) => row.original)}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false)
+ onSuccess?.()
+ }}
+ />
+ ) : null}
+
+ <DocumentClassAddDialog onSuccess={onSuccess} />
+ </div>
+ )
+} \ 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<DataTableRowAction<typeof documentClasses.$inferSelect> | null>(null)
+ const [selectedDocumentClass, setSelectedDocumentClass] = React.useState<typeof documentClasses.$inferSelect | null>(null)
+
+ const refreshData = React.useCallback(() => {
+ window.location.reload()
+ }, [])
+
+ const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])
+
+ // 고급 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<typeof documentClasses.$inferSelect>[] = [
+ { 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 (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <DocumentClassTableToolbarActions table={table} onSuccess={refreshData} />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ {/* 구분선 */}
+ <div className="border-t border-border my-6"></div>
+
+ {/* Document Class 옵션 관리 제목 */}
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">Document Class 옵션 관리</h2>
+ </div>
+ <p className="text-muted-foreground">
+ Document Class 옵션들을 관리합니다.
+ </p>
+ </div>
+ </div>
+
+ {/* Document Class 옵션 테이블 */}
+ <DocumentClassOptionsTable
+ selectedDocumentClass={selectedDocumentClass}
+ documentClasses={rawData[0].data || []}
+ onSelectDocumentClass={setSelectedDocumentClass}
+ />
+
+ <DeleteDocumentClassDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ documentClasses={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ refreshData()
+ }}
+ />
+
+ <DocumentClassEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ data={rowAction?.row.original ?? null}
+ onSuccess={refreshData}
+ />
+ </>
+ )
+} \ No newline at end of file