summaryrefslogtreecommitdiff
path: root/lib/docu-list-rule/code-groups/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/code-groups/table
parent738f956aa61264ffa761e30398eca23393929f8c (diff)
(박서영) 설계 document Numbering Rule 개발-최겸 업로드
Diffstat (limited to 'lib/docu-list-rule/code-groups/table')
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx236
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx259
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx190
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx38
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table.tsx110
-rw-r--r--lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx203
6 files changed, 1036 insertions, 0 deletions
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<typeof createCodeGroupSchema>
+
+interface CodeGroupsAddDialogProps {
+ onSuccess?: () => void
+}
+
+export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ const form = useForm<CreateCodeGroupFormValues>({
+ 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 (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Plus className="mr-2 h-4 w-4" />
+ Add
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="w-[400px] sm:w-[540px]">
+ <DialogHeader>
+ <DialogTitle>Code Group 생성</DialogTitle>
+ <DialogDescription>
+ 새로운 Code Group을 생성합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="description"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Description</FormLabel>
+ <FormControl>
+ <Input placeholder="예: PROJECT NO" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="codeFormat"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Code Format</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="예: AANNN (A:영어대문자, N:숫자)"
+ {...field}
+ onBlur={() => form.trigger('codeFormat')}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="controlType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Control Type</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Control Type을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="textbox">Textbox</SelectItem>
+ <SelectItem value="combobox">Combobox</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+
+ </form>
+ </Form>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleCancel}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ onClick={form.handleSubmit(onSubmit)}
+ disabled={isLoading}
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ {isLoading ? "생성 중..." : "생성"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ 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<typeof updateCodeGroupSchema>
+
+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<UpdateCodeGroupFormValues>({
+ 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 (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-md">
+ <SheetHeader className="text-left">
+ <SheetTitle>Code Group 수정</SheetTitle>
+ <SheetDescription>
+ Code Group 정보를 수정하고 변경사항을 저장하세요
+ </SheetDescription>
+ </SheetHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ <FormField
+ control={form.control}
+ name="description"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Description</FormLabel>
+ <FormControl>
+ <Input placeholder="예: PROJECT NO" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="codeFormat"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Code Format</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="예: AANNN (A:영어대문자, N:숫자)"
+ {...field}
+ onBlur={() => form.trigger('codeFormat')}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="controlType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Control Type</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Control Type을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="textbox">Textbox</SelectItem>
+ <SelectItem value="combobox">Combobox</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="isActive"
+ render={({ field }) => (
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+ <div className="space-y-0.5">
+ <FormLabel className="text-base">활성 상태</FormLabel>
+ </div>
+ <FormControl>
+ <Switch
+ checked={field.value}
+ onCheckedChange={field.onChange}
+ />
+ </FormControl>
+ </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/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<React.SetStateAction<DataTableRowAction<typeof codeGroups.$inferSelect> | null>>
+}
+
+/**
+ * tanstack table 컬럼 정의
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof codeGroups.$inferSelect>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<typeof codeGroups.$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: 20,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<typeof codeGroups.$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: 20,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<typeof codeGroups.$inferSelect>[] = [
+ {
+ accessorKey: "groupId",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Group ID" />
+ ),
+ meta: {
+ excelHeader: "Group ID",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("groupId") ?? "",
+ minSize: 60
+ },
+ {
+ accessorKey: "description",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Description" />
+ ),
+ meta: {
+ excelHeader: "Description",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("description") ?? "",
+ minSize: 80
+ },
+ {
+ accessorKey: "codeFormat",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Code Format" />
+ ),
+ meta: {
+ excelHeader: "Code Format",
+ type: "text",
+ },
+ cell: ({ row }) => row.getValue("codeFormat") ?? "",
+ minSize: 70
+ },
+
+ {
+ accessorKey: "controlType",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Control Type" />
+ ),
+ meta: {
+ excelHeader: "Control Type",
+ type: "text",
+ },
+ cell: ({ row }) => {
+ const controlType = row.getValue("controlType") as string
+ return (
+ <Badge variant="outline">
+ {controlType}
+ </Badge>
+ )
+ },
+ minSize: 70
+ },
+ {
+ accessorKey: "createdAt",
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Created At" />
+ ),
+ 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<TData> {
+ table: Table<TData>
+ onSuccess?: () => void
+}
+
+export function CodeGroupsTableToolbarActions<TData>({
+ table,
+ onSuccess,
+}: CodeGroupsTableToolbarActionsProps<TData>) {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ const selectedCodeGroups = selectedRows.map((row) => row.original as typeof codeGroups.$inferSelect)
+
+ return (
+ <div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {selectedCodeGroups.length > 0 ? (
+ <DeleteCodeGroupsDialog
+ codeGroups={selectedCodeGroups}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false)
+ onSuccess?.()
+ }}
+ />
+ ) : null}
+
+ <CodeGroupsAddDialog onSuccess={onSuccess} />
+ </div>
+ )
+} \ 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<DataTableRowAction<typeof codeGroups.$inferSelect> | 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<typeof codeGroups.$inferSelect>[] = [];
+
+ // 고급 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<typeof codeGroups.$inferSelect>[] = [
+ { 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 (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <CodeGroupsTableToolbarActions table={table} onSuccess={refreshData} />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ <DeleteCodeGroupsDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ codeGroups={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ refreshData()
+ }}
+ />
+
+ <CodeGroupsEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => 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<typeof Dialog> {
+ codeGroups: Row<typeof codeGroups.$inferSelect>["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 (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ delete ({codeGroups.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
+ <DialogDescription className="space-y-2">
+ <p>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{codeGroups.length}</span>
+ 개의 Code Group을 서버에서 영구적으로 삭제합니다.
+ </p>
+
+ {hasComboBoxGroups && (
+ <div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-md">
+ <p className="text-sm font-medium text-amber-800 mb-1">
+ ⚠️ Combo Box 옵션 삭제 경고
+ </p>
+ <p className="text-sm text-amber-700">
+ 다음 {comboBoxGroups.length}개의 Combo Box Code Group이 포함되어 있습니다:
+ </p>
+ <ul className="text-sm text-amber-700 mt-1 ml-4 list-disc">
+ {comboBoxGroups.map((group) => (
+ <li key={group.id}>
+ <strong>{group.description}</strong> ({group.groupId})
+ </li>
+ ))}
+ </ul>
+ <p className="text-sm text-amber-700 mt-2">
+ 이 Code Group들을 삭제하면 관련된 모든 Combo Box 옵션들도 함께 삭제됩니다.
+ </p>
+ </div>
+ )}
+ </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" />
+ delete ({codeGroups.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription className="space-y-2">
+ <p>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{codeGroups.length}</span>
+ 개의 Code Group을 서버에서 영구적으로 삭제합니다.
+ </p>
+
+ {hasComboBoxGroups && (
+ <div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-md">
+ <p className="text-sm font-medium text-amber-800 mb-1">
+ ⚠️ Combo Box 옵션 삭제 경고
+ </p>
+ <p className="text-sm text-amber-700">
+ 다음 {comboBoxGroups.length}개의 Combo Box Code Group이 포함되어 있습니다:
+ </p>
+ <ul className="text-sm text-amber-700 mt-1 ml-4 list-disc">
+ {comboBoxGroups.map((group) => (
+ <li key={group.id}>
+ <strong>{group.description}</strong> ({group.groupId})
+ </li>
+ ))}
+ </ul>
+ <p className="text-sm text-amber-700 mt-2">
+ 이 Code Group들을 삭제하면 관련된 모든 Combo Box 옵션들도 함께 삭제됩니다.
+ </p>
+ </div>
+ )}
+ </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" />
+ )}
+ delete
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+} \ No newline at end of file