diff options
| author | 0-Zz-ang <s1998319@gmail.com> | 2025-08-22 13:47:37 +0900 |
|---|---|---|
| committer | 0-Zz-ang <s1998319@gmail.com> | 2025-08-22 13:47:37 +0900 |
| commit | fefca6304eefea94f41057f9f934b0e19ceb54bb (patch) | |
| tree | f4914faa83e242a68d27feac58ebf0c527302cd2 /lib/compliance/table | |
| parent | dbdae213e39b82ff8ee565df0774bd2f72f06140 (diff) | |
(박서영)Compliance 설문/응답 리스트 생성
Diffstat (limited to 'lib/compliance/table')
6 files changed, 962 insertions, 0 deletions
diff --git a/lib/compliance/table/compliance-survey-templates-columns.tsx b/lib/compliance/table/compliance-survey-templates-columns.tsx new file mode 100644 index 00000000..a5919447 --- /dev/null +++ b/lib/compliance/table/compliance-survey-templates-columns.tsx @@ -0,0 +1,176 @@ +"use client"; + +import * as React from "react"; +import { type ColumnDef } from "@tanstack/react-table"; +import { format } from "date-fns"; +import { ko } from "date-fns/locale"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuShortcut, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react"; +import type { DataTableRowAction } from "@/types/table"; +import { complianceSurveyTemplates } from "@/db/schema/compliance"; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof complianceSurveyTemplates.$inferSelect> | null>>; +} + +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof complianceSurveyTemplates.$inferSelect>[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<typeof complianceSurveyTemplates.$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" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }; + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<typeof complianceSurveyTemplates.$inferSelect> = { + id: "actions", + header: "작업", + enableHiding: false, + cell: ({ row }) => { + const template = row.original; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">메뉴 열기</span> + <MoreHorizontal className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={() => setRowAction({ type: 'update', row: row })}> + <Edit className="mr-2 h-4 w-4" /> + Edit + </DropdownMenuItem> + <DropdownMenuItem onClick={() => setRowAction({ type: 'delete', row: row })}> + <Trash2 className="mr-2 h-4 w-4" /> + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + <DropdownMenuItem onClick={() => window.location.href = `/evcp/compliance/${template.id}`}> + <Eye className="mr-2 h-4 w-4" /> + Detail + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ); + }, + size: 40, + }; + + // ---------------------------------------------------------------- + // 3) 일반 컬럼들 (정렬 가능) + // ---------------------------------------------------------------- + const dataColumns: ColumnDef<typeof complianceSurveyTemplates.$inferSelect>[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="템플릿명" /> + ), + cell: ({ row }) => ( + <div className="font-medium">{row.getValue("name")}</div> + ), + enableResizing: true, + }, + { + accessorKey: "description", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="설명" /> + ), + cell: ({ row }) => ( + <div className="max-w-md truncate">{row.getValue("description")}</div> + ), + enableResizing: true, + }, + { + accessorKey: "version", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="버전" /> + ), + cell: ({ row }) => ( + <Badge variant="secondary">{row.getValue("version")}</Badge> + ), + enableResizing: true, + }, + { + accessorKey: "isActive", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + cell: ({ row }) => { + const isActive = row.getValue("isActive") as boolean; + return ( + <Badge variant={isActive ? "default" : "secondary"}> + {isActive ? "활성" : "비활성"} + </Badge> + ); + }, + enableResizing: true, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return date ? format(new Date(date), 'yyyy-MM-dd', { locale: ko }) : '-'; + }, + enableResizing: true, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("updatedAt") as Date; + return date ? format(new Date(date), 'yyyy-MM-dd', { locale: ko }) : '-'; + }, + enableResizing: true, + }, + ]; + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ]; +} diff --git a/lib/compliance/table/compliance-survey-templates-table.tsx b/lib/compliance/table/compliance-survey-templates-table.tsx new file mode 100644 index 00000000..c2e441ec --- /dev/null +++ b/lib/compliance/table/compliance-survey-templates-table.tsx @@ -0,0 +1,156 @@ +"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, + DataTableFilterField, +} from "@/types/table" +import { getColumns } from "./compliance-survey-templates-columns"; +import { ComplianceTemplateEditSheet } from "./compliance-template-edit-sheet"; +import { DeleteComplianceTemplatesDialog } from "./delete-compliance-templates-dialog"; +import { ComplianceSurveyTemplatesToolbarActions } from "./compliance-survey-templates-toolbar"; +import { complianceSurveyTemplates } from "@/db/schema/compliance"; +import { getComplianceSurveyTemplatesWithSorting } from "../services"; + +interface ComplianceSurveyTemplatesTableProps { + promises?: Promise<[{ data: typeof complianceSurveyTemplates.$inferSelect[]; pageCount: number }] >; +} + +export function ComplianceSurveyTemplatesTable({ promises }: ComplianceSurveyTemplatesTableProps) { + // 페이지네이션 모드 데이터 + const paginationData = promises ? React.use(promises) : null; + const initialData = paginationData ? paginationData[0].data : []; + const pageCount = paginationData ? paginationData[0].pageCount : 0; + + + const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof complianceSurveyTemplates.$inferSelect> | null>(null); + const [data, setData] = React.useState(initialData); + const [currentSorting, setCurrentSorting] = React.useState<{ id: string; desc: boolean }[]>([]); + + // 초기 데이터가 변경되면 data 상태 업데이트 + React.useEffect(() => { + setData(initialData); + }, [initialData]); + + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // 기본 필터 필드 설정 + const filterFields: DataTableFilterField<typeof complianceSurveyTemplates.$inferSelect>[] = [ + { + id: "isActive", + label: "상태", + options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ], + }, + ]; + + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField<typeof complianceSurveyTemplates.$inferSelect>[] = [ + { id: "name", label: "템플릿명", type: "text" }, + { + id: "isActive", label: "상태", type: "select", options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ] + }, + { id: "version", label: "버전", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ]; + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + enableRowSelection: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + // 정렬 상태 변경 감지 + React.useEffect(() => { + const newSorting = table.getState().sorting; + if (JSON.stringify(newSorting) !== JSON.stringify(currentSorting)) { + setCurrentSorting(newSorting); + } + }, [table.getState().sorting, currentSorting]); + + // 정렬이 변경될 때 데이터 다시 로드 + React.useEffect(() => { + const loadData = async () => { + try { + console.log("🔄 정렬 변경으로 데이터 다시 로드:", currentSorting); + + // 정렬 상태가 있으면 정렬된 데이터 가져오기 + if (currentSorting && currentSorting.length > 0) { + const result = await getComplianceSurveyTemplatesWithSorting(currentSorting); + setData(result.data); + } else { + // 기본 정렬로 데이터 가져오기 + const result = await getComplianceSurveyTemplatesWithSorting(); + setData(result.data); + } + } catch (error) { + console.error("데이터 로드 오류:", error); + } + }; + + if (currentSorting.length > 0) { + loadData(); + } + }, [currentSorting]); + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <ComplianceSurveyTemplatesToolbarActions table={table} /> + </DataTableAdvancedToolbar> + </DataTable> + + {/* Edit Sheet */} + {rowAction?.type === 'update' && rowAction.row && ( + <ComplianceTemplateEditSheet + template={rowAction.row.original} + open={true} + onOpenChange={(open) => { + if (!open) setRowAction(null); + }} + /> + )} + + {/* Delete Dialog */} + {rowAction?.type === 'delete' && rowAction.row && ( + <DeleteComplianceTemplatesDialog + templates={[rowAction.row.original]} + showTrigger={false} + open={true} + onOpenChange={(open) => { + if (!open) setRowAction(null); + }} + /> + )} + </> + ); +} diff --git a/lib/compliance/table/compliance-survey-templates-toolbar.tsx b/lib/compliance/table/compliance-survey-templates-toolbar.tsx new file mode 100644 index 00000000..e093550c --- /dev/null +++ b/lib/compliance/table/compliance-survey-templates-toolbar.tsx @@ -0,0 +1,48 @@ +"use client"; + +import * as React from "react"; +import { type Table } from "@tanstack/react-table"; +import { Download } from "lucide-react"; + +import { exportTableToExcel } from "@/lib/export"; +import { Button } from "@/components/ui/button"; +import { ComplianceTemplateCreateDialog } from "./compliance-template-create-dialog"; +import { DeleteComplianceTemplatesDialog } from "./delete-compliance-templates-dialog"; +import { complianceSurveyTemplates } from "@/db/schema/compliance"; + +interface ComplianceSurveyTemplatesToolbarActionsProps { + table: Table<typeof complianceSurveyTemplates.$inferSelect>; +} + +export function ComplianceSurveyTemplatesToolbarActions({ table }: ComplianceSurveyTemplatesToolbarActionsProps) { + return ( + <div className="flex items-center gap-2"> + {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + <DeleteComplianceTemplatesDialog + templates={table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original)} + /> + ) : null} + + <ComplianceTemplateCreateDialog /> + + {/** 3) Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "compliance-survey-templates", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + </div> + ); +} diff --git a/lib/compliance/table/compliance-template-create-dialog.tsx b/lib/compliance/table/compliance-template-create-dialog.tsx new file mode 100644 index 00000000..4d16b0a1 --- /dev/null +++ b/lib/compliance/table/compliance-template-create-dialog.tsx @@ -0,0 +1,191 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" + +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 { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { Plus, Loader2 } from "lucide-react" +import { toast } from "sonner" +import { createComplianceSurveyTemplate } from "../services" + +const createTemplateSchema = z.object({ + name: z.string().min(1, "템플릿명을 입력해주세요.").max(100, "템플릿명은 100자 이하여야 합니다."), + description: z.string().min(1, "설명을 입력해주세요.").max(500, "설명은 500자 이하여야 합니다."), + version: z.string().min(1, "버전을 입력해주세요.").max(20, "버전은 20자 이하여야 합니다."), + isActive: z.boolean().default(true), +}) + +type CreateTemplateFormValues = z.infer<typeof createTemplateSchema> + +export function ComplianceTemplateCreateDialog() { + const [open, setOpen] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + + // react-hook-form 세팅 + const form = useForm<CreateTemplateFormValues>({ + resolver: zodResolver(createTemplateSchema), + defaultValues: { + name: "", + description: "", + version: "1.0", + isActive: true, + }, + mode: "onChange", + }) + + async function onSubmit(data: CreateTemplateFormValues) { + setIsSubmitting(true) + try { + const result = await createComplianceSurveyTemplate(data) + if (result) { + toast.success("새로운 설문조사 템플릿이 생성되었습니다.") + form.reset() + setOpen(false) + // 페이지 새로고침으로 데이터 업데이트 + window.location.reload() + } + } catch (error) { + console.error("템플릿 생성 오류:", error) + toast.error("템플릿 생성에 실패했습니다.") + } finally { + setIsSubmitting(false) + } + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + } + setOpen(nextOpen) + } + + return ( + <Dialog open={open} onOpenChange={handleDialogOpenChange}> + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Plus className="mr-2 h-4 w-4" /> + 템플릿 추가 + </Button> + </DialogTrigger> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle>새 설문조사 템플릿 생성</DialogTitle> + <DialogDescription> + 새로운 준법 설문조사 템플릿을 생성합니다. 템플릿 생성 후 질문을 추가할 수 있습니다. + </DialogDescription> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>템플릿명 *</FormLabel> + <FormControl> + <Input + placeholder="예: ESG 준법 설문조사" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명 *</FormLabel> + <FormControl> + <Textarea + placeholder="템플릿의 목적과 대상에 대한 설명을 입력하세요." + className="min-h-[80px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="version" + render={({ field }) => ( + <FormItem> + <FormLabel>버전 *</FormLabel> + <FormControl> + <Input + placeholder="예: 1.0" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="isActive" + render={({ field }) => ( + <FormItem className="flex items-center space-x-2"> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <FormLabel>활성 상태</FormLabel> + </FormItem> + )} + /> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + type="submit" + disabled={isSubmitting} + > + {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isSubmitting ? "생성 중..." : "템플릿 생성"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +} diff --git a/lib/compliance/table/compliance-template-edit-sheet.tsx b/lib/compliance/table/compliance-template-edit-sheet.tsx new file mode 100644 index 00000000..3ac4870a --- /dev/null +++ b/lib/compliance/table/compliance-template-edit-sheet.tsx @@ -0,0 +1,182 @@ +"use client"; + +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { complianceSurveyTemplates } from "@/db/schema/compliance"; +import { updateComplianceSurveyTemplate } from "@/lib/compliance/services"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; + +const templateSchema = z.object({ + name: z.string().min(1, "템플릿명을 입력하세요"), + description: z.string().min(1, "설명을 입력하세요"), + version: z.string().min(1, "버전을 입력하세요"), + isActive: z.boolean(), +}); + +type TemplateFormData = z.infer<typeof templateSchema>; + +interface ComplianceTemplateEditSheetProps { + template: typeof complianceSurveyTemplates.$inferSelect; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function ComplianceTemplateEditSheet({ + template, + open, + onOpenChange +}: ComplianceTemplateEditSheetProps) { + const [isLoading, setIsLoading] = React.useState(false); + const router = useRouter(); + + const form = useForm<TemplateFormData>({ + resolver: zodResolver(templateSchema), + defaultValues: { + name: template.name, + description: template.description, + version: template.version, + isActive: template.isActive, + }, + }); + + const onSubmit = async (data: TemplateFormData) => { + try { + setIsLoading(true); + + await updateComplianceSurveyTemplate(template.id, data); + + toast.success("템플릿이 성공적으로 수정되었습니다."); + onOpenChange(false); + + // 페이지 새로고침 + router.refresh(); + } catch (error) { + console.error("Error updating template:", error); + toast.error("템플릿 수정 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( + <Sheet open={open} onOpenChange={onOpenChange}> + <SheetContent> + <SheetHeader> + <SheetTitle>템플릿 수정</SheetTitle> + <SheetDescription> + 템플릿 정보를 수정합니다. + </SheetDescription> + </SheetHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>템플릿명</FormLabel> + <FormControl> + <Input placeholder="템플릿명을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명</FormLabel> + <FormControl> + <Textarea + placeholder="템플릿 설명을 입력하세요" + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="version" + render={({ field }) => ( + <FormItem> + <FormLabel>버전</FormLabel> + <FormControl> + <Input placeholder="버전을 입력하세요" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="isActive" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel>활성 상태</FormLabel> + <FormDescription> + 템플릿을 활성화하여 사용할 수 있도록 설정 + </FormDescription> + </div> + </FormItem> + )} + /> + + <SheetFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button type="submit" disabled={isLoading}> + {isLoading ? "수정 중..." : "템플릿 수정"} + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ); +} diff --git a/lib/compliance/table/delete-compliance-templates-dialog.tsx b/lib/compliance/table/delete-compliance-templates-dialog.tsx new file mode 100644 index 00000000..4cc672c7 --- /dev/null +++ b/lib/compliance/table/delete-compliance-templates-dialog.tsx @@ -0,0 +1,209 @@ +"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 { deleteComplianceSurveyTemplate, getTemplatesRelatedDataCount } from "../services" +import { complianceSurveyTemplates } from "@/db/schema/compliance" + +interface DeleteComplianceTemplatesDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + templates: Row<typeof complianceSurveyTemplates.$inferSelect>["original"][] + showTrigger?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export function DeleteComplianceTemplatesDialog({ + templates, + showTrigger = true, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, +}: DeleteComplianceTemplatesDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const [internalOpen, setInternalOpen] = React.useState(false) + const [relatedDataCount, setRelatedDataCount] = React.useState<{ + totalQuestions: number; + totalResponses: number; + }>({ totalQuestions: 0, totalResponses: 0 }) + const isDesktop = useMediaQuery("(min-width: 640px)") + + // controlled/uncontrolled 상태 관리 + const isControlled = controlledOpen !== undefined + const open = isControlled ? controlledOpen : internalOpen + const setOpen = isControlled ? controlledOnOpenChange : setInternalOpen + + // 다이얼로그가 열릴 때 연결된 데이터 개수 조회 + React.useEffect(() => { + + if (open) { + const fetchRelatedDataCount = async () => { + try { + const templateIds = templates.map(template => template.id) + + const data = await getTemplatesRelatedDataCount(templateIds) + + setRelatedDataCount({ + totalQuestions: data.totalQuestions, + totalResponses: data.totalResponses, + }) + } catch (error) { + console.error("Error fetching related data count:", error) + } + } + fetchRelatedDataCount() + } + }, [open, templates]) + + function onDelete() { + startDeleteTransition(async () => { + try { + + + // 각 템플릿을 순차적으로 삭제 + for (const template of templates) { + try { + await deleteComplianceSurveyTemplate(template.id); + } catch (error) { + toast.error(`템플릿 ${template.name} 삭제 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + return; + } + } + + if (setOpen) { + setOpen(false); + } + toast.success("템플릿이 성공적으로 삭제되었습니다."); + + // 페이지 새로고침으로 데이터 업데이트 + window.location.reload(); + } catch (error) { + console.error("Error during deletion:", error); + toast.error("템플릿 삭제 중 오류가 발생했습니다."); + } + }); + } + + if (isDesktop) { + return ( + <Dialog open={open} onOpenChange={setOpen}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + Delete ({templates.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>Are you sure you want to delete?</DialogTitle> + <DialogDescription> + <br /> + This action cannot be undone. + <br /> + This will permanently delete{" "} + <span className="font-medium">{templates.length}</span> + template(s) from the server. + <div className="mt-2 text-sm text-red-600"> + <div><br />⚠️ Data that will be deleted together ⚠️</div> + <div>• Questions: {relatedDataCount.totalQuestions}</div> + <div>• Responses: {relatedDataCount.totalResponses}</div> + </div> + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">Cancel</Button> + </DialogClose> + <Button + aria-label="Delete selected rows" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + Delete + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer open={open} onOpenChange={setOpen}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + Delete ({templates.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>Are you sure you want to delete?</DrawerTitle> + <DrawerDescription> + This action cannot be undone. + <br /> + This will permanently delete{" "} + <span className="font-medium">{templates.length}</span> + template(s) from the server. + <div className="mt-2 text-sm text-red-600"> + <div>⚠️ Data that will be deleted together ⚠️</div> + <div>• Questions: {relatedDataCount.totalQuestions}</div> + <div>• Responses: {relatedDataCount.totalResponses}</div> + </div> + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">Cancel</Button> + </DrawerClose> + <Button + aria-label="Delete selected rows" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + Delete + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +} |
