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 --- lib/docu-list-rule/document-class/service.ts | 462 +++++++++++++++++++++ .../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 +++++ lib/docu-list-rule/document-class/validation.ts | 12 + 14 files changed, 2050 insertions(+) create mode 100644 lib/docu-list-rule/document-class/service.ts 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 create mode 100644 lib/docu-list-rule/document-class/validation.ts (limited to 'lib/docu-list-rule/document-class') diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts new file mode 100644 index 00000000..04dfa50e --- /dev/null +++ b/lib/docu-list-rule/document-class/service.ts @@ -0,0 +1,462 @@ +"use server" + +import { revalidatePath } from "next/cache" +import db from "@/db/db" +import { documentClasses, documentClassOptions, codeGroups } from "@/db/schema/docu-list-rule" +import { eq, desc, asc, sql } from "drizzle-orm" + +// Document Class 목록 조회 (A Class, B Class 등) +export async function getDocumentClassCodeGroups(input: { + page: number + perPage: number + search?: string + sort?: Array<{ id: string; desc: boolean }> + filters?: Array<{ id: string; value: string }> + joinOperator?: "and" | "or" + flags?: string[] + classId?: string + description?: string + isActive?: string +}) { + try { + const { page, perPage, sort, search } = input + const offset = (page - 1) * perPage + + // 기본 조건 + let whereConditions = sql`${documentClasses.isActive} = true` + + // 검색 조건 + if (search) { + const searchTerm = `%${search}%` + whereConditions = sql`${whereConditions} AND ( + ${documentClasses.code} ILIKE ${searchTerm} OR + ${documentClasses.value} ILIKE ${searchTerm} OR + ${documentClasses.description} ILIKE ${searchTerm} + )` + } + + // 정렬 + let orderBy = sql`${documentClasses.createdAt} DESC` + if (sort && sort.length > 0) { + const sortField = sort[0] + const direction = sortField.desc ? sql`DESC` : sql`ASC` + + switch (sortField.id) { + case "code": + orderBy = sql`${documentClasses.code} ${direction}` + break + case "value": + orderBy = sql`${documentClasses.value} ${direction}` + break + case "description": + orderBy = sql`${documentClasses.description} ${direction}` + break + case "createdAt": + orderBy = sql`${documentClasses.createdAt} ${direction}` + break + default: + orderBy = sql`${documentClasses.createdAt} DESC` + } + } + + // 데이터 조회 + const data = await db + .select({ + id: documentClasses.id, + code: documentClasses.code, + value: documentClasses.value, + description: documentClasses.description, + isActive: documentClasses.isActive, + createdAt: documentClasses.createdAt, + updatedAt: documentClasses.updatedAt, + }) + .from(documentClasses) + .where(whereConditions) + .orderBy(orderBy) + .limit(perPage) + .offset(offset) + + // 총 개수 조회 + const [{ count: total }] = await db + .select({ count: sql`count(*)` }) + .from(documentClasses) + .where(whereConditions) + + const pageCount = Math.ceil(Number(total) / perPage) + + return { + success: true, + data, + pageCount, + } + } catch (error) { + console.error("Error fetching document classes:", error) + return { + success: false, + error: "Failed to fetch document classes", + data: [], + pageCount: 0, + } + } +} + +// Document Class 생성 +export async function createDocumentClassCodeGroup(input: { + value: string + description?: string +}) { + try { + // Value 자동 변환: "A", "AB", "A Class", "A CLASS" 등을 "A Class", "AB Class" 형태로 변환 + const formatValue = (value: string): string => { + // 공백 제거 및 대소문자 정규화 + const cleaned = value.trim().toLowerCase() + + // "class"가 포함되어 있으면 제거 + const withoutClass = cleaned.replace(/\s*class\s*/g, '') + + // 알파벳과 숫자만 추출 + const letters = withoutClass.replace(/[^a-z0-9]/g, '') + + if (letters.length === 0) { + return value.trim() // 변환할 수 없으면 원본 반환 + } + + // 첫 글자를 대문자로 변환하고 "Class" 추가 + return letters.charAt(0).toUpperCase() + letters.slice(1) + " Class" + } + + const formattedValue = formatValue(input.value) + + // 자동으로 code 생성 (예: "DOC_CLASS_001", "DOC_CLASS_002" 등) + const existingClasses = await db + .select({ code: documentClasses.code }) + .from(documentClasses) + .orderBy(desc(documentClasses.code)) + + let newCode = "DOC_CLASS_001" + if (existingClasses.length > 0) { + const lastClass = existingClasses[0] + if (lastClass.code) { + const lastNumber = parseInt(lastClass.code.replace("DOC_CLASS_", "")) || 0 + newCode = `DOC_CLASS_${String(lastNumber + 1).padStart(3, '0')}` + } + } + + // Code Group이 존재하는지 확인 + const existingCodeGroup = await db + .select({ id: codeGroups.id }) + .from(codeGroups) + .where(eq(codeGroups.groupId, 'DOC_CLASS')) + .limit(1) + + let codeGroupId: number | null = null + + if (existingCodeGroup.length === 0) { + // Code Group이 없으면 자동으로 생성 + const [newCodeGroup] = await db + .insert(codeGroups) + .values({ + groupId: 'DOC_CLASS', + description: 'Document Class', + codeFormat: 'DOC_CLASS_###', + expressions: '^DOC_CLASS_\\d{3}$', + controlType: 'Combobox', + isActive: true, + }) + .returning({ id: codeGroups.id }) + + codeGroupId = newCodeGroup.id + } else { + codeGroupId = existingCodeGroup[0].id + } + + const [newDocumentClass] = await db + .insert(documentClasses) + .values({ + code: newCode, + value: formattedValue, + description: input.description || "", + codeGroupId: codeGroupId, + isActive: true, + }) + .returning({ id: documentClasses.id }) + + revalidatePath("/evcp/docu-list-rule/document-class") + revalidatePath("/evcp/docu-list-rule/code-groups") + + return { + success: true, + data: newDocumentClass, + message: "Document Class created successfully" + } + } catch (error) { + console.error("Error creating document class:", error) + return { + success: false, + error: "Failed to create document class" + } + } +} + +// Document Class 수정 +export async function updateDocumentClassCodeGroup(input: { + id: number + value: string + description?: string +}) { + try { + // Value 자동 변환: "A", "AB", "A Class", "A CLASS" 등을 "A Class", "AB Class" 형태로 변환 + const formatValue = (value: string): string => { + // 공백 제거 및 대소문자 정규화 + const cleaned = value.trim().toLowerCase() + + // "class"가 포함되어 있으면 제거 + const withoutClass = cleaned.replace(/\s*class\s*/g, '') + + // 알파벳과 숫자만 추출 + const letters = withoutClass.replace(/[^a-z0-9]/g, '') + + if (letters.length === 0) { + return value.trim() // 변환할 수 없으면 원본 반환 + } + + // 첫 글자를 대문자로 변환하고 "Class" 추가 + return letters.charAt(0).toUpperCase() + letters.slice(1) + " Class" + } + + const formattedValue = formatValue(input.value) + + const [updatedDocumentClass] = await db + .update(documentClasses) + .set({ + value: formattedValue, + description: input.description || "", + updatedAt: new Date(), + }) + .where(eq(documentClasses.id, input.id)) + .returning({ id: documentClasses.id }) + + revalidatePath("/evcp/docu-list-rule/document-class") + + return { + success: true, + data: updatedDocumentClass, + message: "Document Class updated successfully" + } + } catch (error) { + console.error("Error updating document class:", error) + return { + success: false, + error: "Failed to update document class" + } + } +} + +// Document Class 삭제 +export async function deleteDocumentClassCodeGroup(id: number) { + try { + // 삭제할 Document Class의 codeGroupId 확인 + const documentClassToDelete = await db + .select({ codeGroupId: documentClasses.codeGroupId }) + .from(documentClasses) + .where(eq(documentClasses.id, id)) + .limit(1) + + const [deletedDocumentClass] = await db + .delete(documentClasses) + .where(eq(documentClasses.id, id)) + .returning({ id: documentClasses.id }) + + // 같은 codeGroupId를 가진 다른 Document Class가 있는지 확인 + if (documentClassToDelete.length > 0 && documentClassToDelete[0].codeGroupId) { + const remainingClasses = await db + .select({ id: documentClasses.id }) + .from(documentClasses) + .where(eq(documentClasses.codeGroupId, documentClassToDelete[0].codeGroupId)) + .limit(1) + + // 더 이상 Document Class가 없으면 Code Group도 삭제 + if (remainingClasses.length === 0) { + await db + .delete(codeGroups) + .where(eq(codeGroups.id, documentClassToDelete[0].codeGroupId)) + } + } + + revalidatePath("/evcp/docu-list-rule/document-class") + revalidatePath("/evcp/docu-list-rule/code-groups") + + return { + success: true, + data: deletedDocumentClass, + message: "Document Class deleted successfully" + } + } catch (error) { + console.error("Error deleting document class:", error) + return { + success: false, + error: "Failed to delete document class" + } + } +} + +// Document Class 옵션 목록 조회 +export async function getDocumentClassSubOptions(documentClassId: number) { + try { + const data = await db + .select({ + id: documentClassOptions.id, + documentClassId: documentClassOptions.documentClassId, + optionValue: documentClassOptions.optionValue, + optionCode: documentClassOptions.optionCode, + sortOrder: documentClassOptions.sortOrder, + isActive: documentClassOptions.isActive, + createdAt: documentClassOptions.createdAt, + updatedAt: documentClassOptions.updatedAt, + }) + .from(documentClassOptions) + .where(eq(documentClassOptions.documentClassId, documentClassId)) + .orderBy(asc(documentClassOptions.sortOrder), asc(documentClassOptions.optionValue)) + + return { + success: true, + data, + } + } catch (error) { + console.error("Error fetching document class options:", error) + return { + success: false, + error: "Failed to fetch document class options", + data: [], + } + } +} + +// Document Class 옵션 생성 +export async function createDocumentClassOptionItem(input: { + documentClassId: number + optionValue: string +}) { + try { + // Document Class 정보 조회하여 Value 가져오기 + const documentClass = await db + .select({ value: documentClasses.value }) + .from(documentClasses) + .where(eq(documentClasses.id, input.documentClassId)) + .limit(1) + + if (!documentClass.length) { + return { + success: false, + error: "Document Class not found" + } + } + + // Value에서 클래스명 추출 (예: "A Class" → "A") + const classValue = documentClass[0].value + const className = classValue.split(' ')[0] // "A Class"에서 "A" 추출 + + // 자동으로 optionCode 생성 (예: "A_OP_01", "A_OP_02" 등) + const existingOptions = await db + .select({ optionCode: documentClassOptions.optionCode }) + .from(documentClassOptions) + .where(eq(documentClassOptions.documentClassId, input.documentClassId)) + .orderBy(desc(documentClassOptions.optionCode)) + + let newOptionCode = `${className}_OP_01` + if (existingOptions.length > 0) { + const lastOption = existingOptions[0] + if (lastOption.optionCode) { + // "A_OP_01" 형태에서 숫자 추출 + const match = lastOption.optionCode.match(/_OP_(\d+)$/) + if (match) { + const lastNumber = parseInt(match[1]) || 0 + newOptionCode = `${className}_OP_${String(lastNumber + 1).padStart(2, '0')}` + } else { + // 기존 형식이 다른 경우 01부터 시작 + newOptionCode = `${className}_OP_01` + } + } + } + + const [newOption] = await db + .insert(documentClassOptions) + .values({ + documentClassId: input.documentClassId, + optionValue: input.optionValue, + optionCode: newOptionCode, + sortOrder: 0, + isActive: true, + }) + .returning({ id: documentClassOptions.id }) + + revalidatePath("/evcp/docu-list-rule/document-class") + + return { + success: true, + data: newOption, + message: "Document Class option created successfully" + } + } catch (error) { + console.error("Error creating document class option:", error) + return { + success: false, + error: "Failed to create document class option" + } + } +} + +// Document Class 옵션 수정 +export async function updateDocumentClassOption(input: { + id: number + optionValue: string +}) { + try { + const [updatedOption] = await db + .update(documentClassOptions) + .set({ + optionValue: input.optionValue, + updatedAt: new Date(), + }) + .where(eq(documentClassOptions.id, input.id)) + .returning({ id: documentClassOptions.id }) + + revalidatePath("/evcp/docu-list-rule/document-class") + + return { + success: true, + data: updatedOption, + message: "Document Class option updated successfully" + } + } catch (error) { + console.error("Error updating document class option:", error) + return { + success: false, + error: "Failed to update document class option" + } + } +} + +// Document Class 옵션 삭제 +export async function deleteDocumentClassOption(id: number) { + try { + const [deletedOption] = await db + .delete(documentClassOptions) + .where(eq(documentClassOptions.id, id)) + .returning({ id: documentClassOptions.id }) + + revalidatePath("/evcp/docu-list-rule/document-class") + + return { + success: true, + data: deletedOption, + message: "Document Class option deleted successfully" + } + } catch (error) { + console.error("Error deleting document class option:", error) + return { + success: false, + error: "Failed to delete document class option" + } + } +} \ No newline at end of file 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 diff --git a/lib/docu-list-rule/document-class/validation.ts b/lib/docu-list-rule/document-class/validation.ts new file mode 100644 index 00000000..0600e8fb --- /dev/null +++ b/lib/docu-list-rule/document-class/validation.ts @@ -0,0 +1,12 @@ +import { createSearchParamsCache } from "nuqs/server"; +import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server"; +import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers"; + +export const searchParamsDocumentClassCache = createSearchParamsCache({ + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser(), + filters: getFiltersStateParser(), + search: parseAsString.withDefault(""), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), +}); \ No newline at end of file -- cgit v1.2.3