summaryrefslogtreecommitdiff
path: root/lib/compliance/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance/table')
-rw-r--r--lib/compliance/table/compliance-survey-templates-columns.tsx176
-rw-r--r--lib/compliance/table/compliance-survey-templates-table.tsx156
-rw-r--r--lib/compliance/table/compliance-survey-templates-toolbar.tsx48
-rw-r--r--lib/compliance/table/compliance-template-create-dialog.tsx191
-rw-r--r--lib/compliance/table/compliance-template-edit-sheet.tsx182
-rw-r--r--lib/compliance/table/delete-compliance-templates-dialog.tsx209
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>
+ )
+}