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/code-groups/service.ts | 290 +++++++++++++++++++++ .../code-groups/table/code-groups-add-dialog.tsx | 236 +++++++++++++++++ .../code-groups/table/code-groups-edit-sheet.tsx | 259 ++++++++++++++++++ .../table/code-groups-table-columns.tsx | 190 ++++++++++++++ .../table/code-groups-table-toolbar.tsx | 38 +++ .../code-groups/table/code-groups-table.tsx | 110 ++++++++ .../table/delete-code-groups-dialog.tsx | 203 +++++++++++++++ lib/docu-list-rule/code-groups/validation.ts | 34 +++ 8 files changed, 1360 insertions(+) create mode 100644 lib/docu-list-rule/code-groups/service.ts create mode 100644 lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx create mode 100644 lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx create mode 100644 lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx create mode 100644 lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx create mode 100644 lib/docu-list-rule/code-groups/table/code-groups-table.tsx create mode 100644 lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx create mode 100644 lib/docu-list-rule/code-groups/validation.ts (limited to 'lib/docu-list-rule/code-groups') diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts new file mode 100644 index 00000000..34ec5610 --- /dev/null +++ b/lib/docu-list-rule/code-groups/service.ts @@ -0,0 +1,290 @@ +"use server" + +import { revalidatePath } from "next/cache" +import db from "@/db/db" +import { codeGroups, comboBoxSettings, documentClasses } from "@/db/schema/docu-list-rule" +import { eq, sql, count } from "drizzle-orm" +import { unstable_noStore } from "next/cache" + +// Code Groups 목록 조회 +export async function getCodeGroups(input: { + page: number + perPage: number + search?: string + sort?: Array<{ id: string; desc: boolean }> + filters?: Array<{ id: string; value: string }> + joinOperator?: "and" | "or" + flags?: string[] + groupId?: string + description?: string + controlType?: string + isActive?: string +} | any) { + unstable_noStore() + + try { + const { page, perPage, sort, search } = input + const offset = (page - 1) * perPage + + // 검색 조건 (Document Class 제외) + let whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS'` + if (search) { + const searchTerm = `%${search}%` + whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS' AND ( + ${codeGroups.groupId} ILIKE ${searchTerm} OR + ${codeGroups.description} ILIKE ${searchTerm} OR + ${codeGroups.codeFormat} ILIKE ${searchTerm} OR + ${codeGroups.controlType} ILIKE ${searchTerm} + )` + } + + // 정렬 + let orderBy = sql`${codeGroups.createdAt} DESC` + if (sort && sort.length > 0) { + const sortField = sort[0] + const direction = sortField.desc ? sql`DESC` : sql`ASC` + + switch (sortField.id) { + case "groupId": + orderBy = sql`${codeGroups.groupId} ${direction}` + break + case "description": + orderBy = sql`${codeGroups.description} ${direction}` + break + case "codeFormat": + orderBy = sql`${codeGroups.codeFormat} ${direction}` + break + case "controlType": + orderBy = sql`${codeGroups.controlType} ${direction}` + break + case "isActive": + orderBy = sql`${codeGroups.isActive} ${direction}` + break + case "createdAt": + orderBy = sql`${codeGroups.createdAt} ${direction}` + break + default: + orderBy = sql`${codeGroups.createdAt} DESC` + } + } + + // 데이터 조회 + const data = await db + .select({ + id: codeGroups.id, + groupId: codeGroups.groupId, + description: codeGroups.description, + codeFormat: codeGroups.codeFormat, + expressions: codeGroups.expressions, + controlType: codeGroups.controlType, + isActive: codeGroups.isActive, + createdAt: codeGroups.createdAt, + updatedAt: codeGroups.updatedAt, + }) + .from(codeGroups) + .where(whereConditions) + .orderBy(orderBy) + .limit(perPage) + .offset(offset) + + // 총 개수 조회 (Document Class 제외) + const [{ count: total }] = await db + .select({ count: count() }) + .from(codeGroups) + .where(whereConditions) + + const pageCount = Math.ceil(total / perPage) + + return { + data, + pageCount, + total, + } + } catch (error) { + console.error("Error fetching code groups:", error) + return { + data: [], + pageCount: 0, + total: 0, + } + } +} + +// Code Group 생성 +export async function createCodeGroup(input: { + description: string + codeFormat?: string + expressions?: string + controlType: string + isActive?: boolean +}) { + try { + // 마지막 Code Group의 groupId를 찾아서 다음 번호 생성 (DOC_CLASS 제외) + const lastCodeGroup = await db + .select({ groupId: codeGroups.groupId }) + .from(codeGroups) + .where(sql`${codeGroups.groupId} != 'DOC_CLASS'`) + .orderBy(sql`CAST(SUBSTRING(${codeGroups.groupId}, 6) AS INTEGER) DESC`) + .limit(1) + + let nextNumber = 1 + if (lastCodeGroup.length > 0 && lastCodeGroup[0].groupId) { + const lastNumber = parseInt(lastCodeGroup[0].groupId.replace('Code_', '')) + if (!isNaN(lastNumber)) { + nextNumber = lastNumber + 1 + } + } + + const newGroupId = `Code_${nextNumber}` + + // 새 Code Group 생성 + const [newCodeGroup] = await db + .insert(codeGroups) + .values({ + groupId: newGroupId, + description: input.description, + codeFormat: input.codeFormat, + expressions: input.expressions, + controlType: input.controlType, + isActive: input.isActive ?? true, + }) + .returning({ id: codeGroups.id, groupId: codeGroups.groupId }) + + revalidatePath("/evcp/docu-list-rule/code-groups") + + return { + success: true, + data: newCodeGroup, + message: "Code Group created successfully" + } + } catch (error) { + console.error("Error creating code group:", error) + return { + success: false, + error: "Failed to create code group" + } + } +} + +// Code Group 수정 +export async function updateCodeGroup(input: { + id: number + description: string + codeFormat?: string + expressions?: string + controlType: string + isActive?: boolean +}) { + try { + const [updatedCodeGroup] = await db + .update(codeGroups) + .set({ + description: input.description, + codeFormat: input.codeFormat, + expressions: input.expressions, + controlType: input.controlType, + isActive: input.isActive, + updatedAt: new Date(), + }) + .where(eq(codeGroups.id, input.id)) + .returning({ id: codeGroups.id }) + + revalidatePath("/evcp/docu-list-rule/code-groups") + + return { + success: true, + data: updatedCodeGroup, + message: "Code Group updated successfully" + } + } catch (error) { + console.error("Error updating code group:", error) + return { + success: false, + error: "Failed to update code group" + } + } +} + +// Code Group 삭제 +export async function deleteCodeGroup(id: number) { + try { + // Code Group 정보 조회 + const codeGroup = await db + .select({ + id: codeGroups.id, + controlType: codeGroups.controlType, + description: codeGroups.description + }) + .from(codeGroups) + .where(eq(codeGroups.id, id)) + .limit(1) + + if (codeGroup.length === 0) { + return { + success: false, + error: "Code Group not found" + } + } + + // Control Type이 combobox인 경우 관련 Combo Box 옵션들도 삭제 + if (codeGroup[0].controlType === 'combobox') { + // Combo Box 옵션들 삭제 + await db + .delete(comboBoxSettings) + .where(eq(comboBoxSettings.codeGroupId, id)) + } + + // Document Class가 연결된 경우 Document Class도 삭제 + await db + .delete(documentClasses) + .where(eq(documentClasses.codeGroupId, id)) + + // Code Group 삭제 + await db + .delete(codeGroups) + .where(eq(codeGroups.id, id)) + + revalidatePath("/evcp/docu-list-rule/code-groups") + revalidatePath("/evcp/docu-list-rule/combo-box-settings") + revalidatePath("/evcp/docu-list-rule/document-class") + + return { + success: true, + message: codeGroup[0].controlType === 'combobox' + ? `Code Group과 관련 Combo Box 옵션들이 삭제되었습니다.` + : "Code Group이 삭제되었습니다." + } + } catch (error) { + console.error("Error deleting code group:", error) + return { + success: false, + error: "Failed to delete code group" + } + } +} + +// Code Group 단일 조회 +export async function getCodeGroupById(id: number) { + try { + const [codeGroup] = await db + .select({ + id: codeGroups.id, + groupId: codeGroups.groupId, + description: codeGroups.description, + codeFormat: codeGroups.codeFormat, + expressions: codeGroups.expressions, + controlType: codeGroups.controlType, + isActive: codeGroups.isActive, + createdAt: codeGroups.createdAt, + updatedAt: codeGroups.updatedAt, + }) + .from(codeGroups) + .where(eq(codeGroups.id, id)) + .limit(1) + + return codeGroup || null + } catch (error) { + console.error("Error fetching code group by id:", error) + return null + } +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx b/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx new file mode 100644 index 00000000..660adfed --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx @@ -0,0 +1,236 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" +import { Plus, Loader2 } 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { createCodeGroup } from "../service" +import { z } from "zod" + +const createCodeGroupSchema = z.object({ + description: z.string().min(1, "Description은 필수입니다."), + codeFormat: z.string().optional().refine((val) => { + if (!val) return true; // 빈 값은 허용 + return /^[AN]*$/.test(val); + }, "Code Format은 A(영어 대문자) 또는 N(숫자)만 입력 가능합니다."), + controlType: z.string().min(1, "Control Type은 필수입니다."), +}) + +type CreateCodeGroupFormValues = z.infer + +interface CodeGroupsAddDialogProps { + onSuccess?: () => void +} + +export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { + const [open, setOpen] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + + const form = useForm({ + resolver: zodResolver(createCodeGroupSchema), + defaultValues: { + description: "", + codeFormat: "", + controlType: "", + }, + }) + + // Code Format을 기반으로 정규식 자동 생성 함수 + const generateExpression = (codeFormat: string): string => { + if (!codeFormat) return '' + + let expression = '^' + let currentChar = codeFormat[0] + let count = 1 + + for (let i = 1; i < codeFormat.length; i++) { + if (codeFormat[i] === currentChar) { + count++ + } else { + // 이전 문자에 대한 정규식 추가 + if (currentChar === 'A') { + expression += `[A-Z]{${count}}` + } else if (currentChar === 'N') { + expression += `[0-9]{${count}}` + } + currentChar = codeFormat[i] + count = 1 + } + } + + // 마지막 문자 처리 + if (currentChar === 'A') { + expression += `[A-Z]{${count}}` + } else if (currentChar === 'N') { + expression += `[0-9]{${count}}` + } + + expression += '$' + return expression + } + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) { + form.reset() + } + } + + const handleCancel = () => { + form.reset() + setOpen(false) + } + + const onSubmit = async (data: CreateCodeGroupFormValues) => { + setIsLoading(true) + try { + // Expression 자동 생성 + const expressions = generateExpression(data.codeFormat || "") + + const result = await createCodeGroup({ + description: data.description, + codeFormat: data.codeFormat, + expressions: expressions, + controlType: data.controlType, + }) + + if (result.success) { + toast.success("Code Group이 성공적으로 생성되었습니다.") + form.reset() + setOpen(false) + onSuccess?.() + } else { + toast.error(result.error || "생성 중 오류가 발생했습니다.") + } + } catch (error) { + console.error("Code Group 생성 오류:", error) + toast.error("Code Group 생성에 실패했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + + + + Code Group 생성 + + 새로운 Code Group을 생성합니다. + + + +
+ + ( + + Description + + + + + + )} + /> + ( + + Code Format + + form.trigger('codeFormat')} + /> + + + + )} + /> + ( + + Control Type + + + + )} + /> + + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx b/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx new file mode 100644 index 00000000..28aebd54 --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx @@ -0,0 +1,259 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" +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 { Switch } from "@/components/ui/switch" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { updateCodeGroup } from "../service" +import { codeGroups } from "@/db/schema/codeGroups" +import { z } from "zod" + +const updateCodeGroupSchema = z.object({ + description: z.string().min(1, "Description은 필수입니다."), + codeFormat: z.string().optional().refine((val) => { + if (!val) return true; // 빈 값은 허용 + return /^[AN]*$/.test(val); + }, "Code Format은 A(영어 대문자) 또는 N(숫자)만 입력 가능합니다."), + controlType: z.string().min(1, "Control Type은 필수입니다."), + isActive: z.boolean().default(true), +}) + +type UpdateCodeGroupFormValues = z.infer + +interface CodeGroupsEditSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + data: typeof codeGroups.$inferSelect | null + onSuccess: () => void +} + +export function CodeGroupsEditSheet({ + open, + onOpenChange, + data, + onSuccess, +}: CodeGroupsEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(updateCodeGroupSchema), + defaultValues: { + description: data?.description ?? "", + codeFormat: data?.codeFormat ?? "", + controlType: data?.controlType ?? "", + isActive: data?.isActive ?? true, + }, + mode: "onChange" + }) + + // Code Format을 기반으로 정규식 자동 생성 함수 + const generateExpression = (codeFormat: string): string => { + if (!codeFormat) return '' + + let expression = '^' + let currentChar = codeFormat[0] + let count = 1 + + for (let i = 1; i < codeFormat.length; i++) { + if (codeFormat[i] === currentChar) { + count++ + } else { + // 이전 문자에 대한 정규식 추가 + if (currentChar === 'A') { + expression += `[A-Z]{${count}}` + } else if (currentChar === 'N') { + expression += `[0-9]{${count}}` + } + currentChar = codeFormat[i] + count = 1 + } + } + + // 마지막 문자 처리 + if (currentChar === 'A') { + expression += `[A-Z]{${count}}` + } else if (currentChar === 'N') { + expression += `[0-9]{${count}}` + } + + expression += '$' + return expression + } + + React.useEffect(() => { + if (data) { + form.reset({ + description: data.description, + codeFormat: data.codeFormat || "", + controlType: data.controlType, + isActive: data.isActive ?? true, + }) + } + }, [data, form]) + + async function onSubmit(input: UpdateCodeGroupFormValues) { + if (!data) return + + startUpdateTransition(async () => { + try { + // Code Format이 변경되면 Expression 자동 업데이트 + const expressions = generateExpression(input.codeFormat || "") + + const result = await updateCodeGroup({ + id: data.id, + description: input.description, + codeFormat: input.codeFormat, + expressions: expressions, + controlType: input.controlType, + isActive: input.isActive, + }) + + if (result.success) { + toast.success("Code Group이 성공적으로 수정되었습니다.") + onSuccess() + onOpenChange(false) + } else { + toast.error(result.error || "Code Group 수정에 실패했습니다.") + } + } catch (error) { + console.error("Update error:", error) + toast.error("Code Group 수정 중 오류가 발생했습니다.") + } + }) + } + + return ( + + + + Code Group 수정 + + Code Group 정보를 수정하고 변경사항을 저장하세요 + + +
+ + ( + + Description + + + + + + )} + /> + ( + + Code Format + + form.trigger('codeFormat')} + /> + + + + )} + /> + + ( + + Control Type + + + + )} + /> + ( + +
+ 활성 상태 +
+ + + +
+ )} + /> + + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx new file mode 100644 index 00000000..cb6cdf8b --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx @@ -0,0 +1,190 @@ +"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 { Badge } from "@/components/ui/badge" +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 { codeGroups } 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: 20, + 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: 20, + } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef[] = [ + { + accessorKey: "groupId", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "Group ID", + type: "text", + }, + cell: ({ row }) => row.getValue("groupId") ?? "", + minSize: 60 + }, + { + accessorKey: "description", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "Description", + type: "text", + }, + cell: ({ row }) => row.getValue("description") ?? "", + minSize: 80 + }, + { + accessorKey: "codeFormat", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "Code Format", + type: "text", + }, + cell: ({ row }) => row.getValue("codeFormat") ?? "", + minSize: 70 + }, + + { + accessorKey: "controlType", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "Control Type", + type: "text", + }, + cell: ({ row }) => { + const controlType = row.getValue("controlType") as string + return ( + + {controlType} + + ) + }, + minSize: 70 + }, + { + accessorKey: "createdAt", + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: "Created At", + type: "date", + }, + cell: ({ row }) => { + const dateVal = row.getValue("createdAt") as Date + return formatDateTime(dateVal, "KR") + }, + minSize: 60 + } + ] + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx new file mode 100644 index 00000000..d2d9efb4 --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx @@ -0,0 +1,38 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" + +import { DeleteCodeGroupsDialog } from "./delete-code-groups-dialog" +import { CodeGroupsAddDialog } from "./code-groups-add-dialog" +import { codeGroups } from "@/db/schema/codeGroups" + +interface CodeGroupsTableToolbarActionsProps { + table: Table + onSuccess?: () => void +} + +export function CodeGroupsTableToolbarActions({ + table, + onSuccess, +}: CodeGroupsTableToolbarActionsProps) { + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedCodeGroups = selectedRows.map((row) => row.original as typeof codeGroups.$inferSelect) + + return ( +
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {selectedCodeGroups.length > 0 ? ( + { + table.toggleAllRowsSelected(false) + onSuccess?.() + }} + /> + ) : null} + + +
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/table/code-groups-table.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table.tsx new file mode 100644 index 00000000..6d8bb907 --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/code-groups-table.tsx @@ -0,0 +1,110 @@ +"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, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" +import { getCodeGroups } from "../service"; +import { getColumns } from "./code-groups-table-columns"; +import { DeleteCodeGroupsDialog } from "./delete-code-groups-dialog"; +import { CodeGroupsEditSheet } from "./code-groups-edit-sheet"; +import { CodeGroupsTableToolbarActions } from "./code-groups-table-toolbar"; +import { codeGroups } from "@/db/schema/docu-list-rule"; + +interface CodeGroupsTableProps { + promises?: Promise<[{ data: typeof codeGroups.$inferSelect[]; pageCount: number }] >; +} + +export function CodeGroupsTable({ promises }: CodeGroupsTableProps) { + const [rowAction, setRowAction] = React.useState | null>(null); + + const [{ data, pageCount }] = promises ? React.use(promises) : [{ data: [], pageCount: 0 }]; + + const refreshData = React.useCallback(async () => { + // 페이지 새로고침으로 처리 + window.location.reload(); + }, []); + + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // 필터 필드 설정 + const filterFields: DataTableFilterField[] = []; + + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "groupId", label: "Group ID", type: "text" }, + { id: "description", label: "Description", type: "text" }, + { id: "codeFormat", label: "Code Format", type: "text" }, + { + id: "controlType", label: "Control Type", type: "select", options: [ + { label: "Textbox", value: "textbox" }, + { label: "Combobox", value: "combobox" }, + { label: "Date", value: "date" }, + { label: "Number", value: "number" }, + ] + }, + { + id: "isActive", label: "Status", type: "select", options: [ + { label: "Active", value: "true" }, + { label: "Inactive", value: "false" }, + ] + }, + { id: "createdAt", label: "Created At", type: "date" }, + ]; + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.groupId), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + + + + + + + setRowAction(null)} + codeGroups={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/code-groups/table/delete-code-groups-dialog.tsx b/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx new file mode 100644 index 00000000..66a8d7c2 --- /dev/null +++ b/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx @@ -0,0 +1,203 @@ +"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 { codeGroups } from "@/db/schema/codeGroups" +import { deleteCodeGroup } from "../service" + +interface DeleteCodeGroupsDialogProps + extends React.ComponentPropsWithoutRef { + codeGroups: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteCodeGroupsDialog({ + codeGroups, + showTrigger = true, + onSuccess, + ...props +}: DeleteCodeGroupsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + // Combo Box Code Group이 있는지 확인 + const hasComboBoxGroups = codeGroups.some(group => group.controlType === 'combobox') + const comboBoxGroups = codeGroups.filter(group => group.controlType === 'combobox') + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 Code Group을 순차적으로 삭제 + for (const codeGroup of codeGroups) { + const result = await deleteCodeGroup(codeGroup.id) + if (!result.success) { + toast.error(`Code Group ${codeGroup.description} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("Code Group이 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("Code Group 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + +

+ 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {codeGroups.length} + 개의 Code Group을 서버에서 영구적으로 삭제합니다. +

+ + {hasComboBoxGroups && ( +
+

+ ⚠️ Combo Box 옵션 삭제 경고 +

+

+ 다음 {comboBoxGroups.length}개의 Combo Box Code Group이 포함되어 있습니다: +

+
    + {comboBoxGroups.map((group) => ( +
  • + {group.description} ({group.groupId}) +
  • + ))} +
+

+ 이 Code Group들을 삭제하면 관련된 모든 Combo Box 옵션들도 함께 삭제됩니다. +

+
+ )} +
+
+ + + + + + +
+
+ ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + +

+ 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {codeGroups.length} + 개의 Code Group을 서버에서 영구적으로 삭제합니다. +

+ + {hasComboBoxGroups && ( +
+

+ ⚠️ Combo Box 옵션 삭제 경고 +

+

+ 다음 {comboBoxGroups.length}개의 Combo Box Code Group이 포함되어 있습니다: +

+
    + {comboBoxGroups.map((group) => ( +
  • + {group.description} ({group.groupId}) +
  • + ))} +
+

+ 이 Code Group들을 삭제하면 관련된 모든 Combo Box 옵션들도 함께 삭제됩니다. +

+
+ )} +
+
+ + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/docu-list-rule/code-groups/validation.ts b/lib/docu-list-rule/code-groups/validation.ts new file mode 100644 index 00000000..90e06d0c --- /dev/null +++ b/lib/docu-list-rule/code-groups/validation.ts @@ -0,0 +1,34 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { codeGroups } from "@/db/schema/docu-list-rule"; + +export const searchParamsCodeGroupsCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([ + { id: "createdAt", desc: true }, + ]), + + groupId: parseAsString.withDefault(""), + description: parseAsString.withDefault(""), + controlType: parseAsString.withDefault(""), + isActive: parseAsString.withDefault(""), + + // advanced filter + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) + +export type GetCodeGroupsSchema = Awaited> \ No newline at end of file -- cgit v1.2.3