summaryrefslogtreecommitdiff
path: root/lib/general-check-list/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-19 09:44:28 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-19 09:44:28 +0000
commit95bbe9c583ff841220da1267630e7b2025fc36dc (patch)
tree5e3d5bb3302530bbaa7f7abbe8c9cf8193ccbd4c /lib/general-check-list/table
parent0eb030580b5cbe5f03d570c3c9d8c519bac3b783 (diff)
(대표님) 20250619 1844 KST 작업사항
Diffstat (limited to 'lib/general-check-list/table')
-rw-r--r--lib/general-check-list/table/add-check-list-dialog.tsx112
-rw-r--r--lib/general-check-list/table/delete-check-lists-dialog.tsx106
-rw-r--r--lib/general-check-list/table/general-check-list-table.tsx63
-rw-r--r--lib/general-check-list/table/general-check-table-columns.tsx138
-rw-r--r--lib/general-check-list/table/update-check-list-sheet.tsx162
5 files changed, 581 insertions, 0 deletions
diff --git a/lib/general-check-list/table/add-check-list-dialog.tsx b/lib/general-check-list/table/add-check-list-dialog.tsx
new file mode 100644
index 00000000..5721bd59
--- /dev/null
+++ b/lib/general-check-list/table/add-check-list-dialog.tsx
@@ -0,0 +1,112 @@
+"use client";
+import * as React from "react";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
+import { Plus } from "lucide-react";
+import { toast } from "sonner";
+import { createGeneralEvaluation } from "@/lib/general-check-list/service";
+import { useRouter } from "next/navigation";
+
+const schema = z.object({
+ category: z.string().min(1, "카테고리를 입력하세요"),
+ inspectionItem: z.string().min(1, "점검 항목을 입력하세요"),
+ remarks: z.string().optional(),
+});
+
+type FormValues = z.infer<typeof schema>;
+
+export function CreateEvaluationDialog({ onSuccess }: { onSuccess?: () => void }) {
+ const [open, setOpen] = React.useState(false);
+ const [pending, setPending] = React.useState(false);
+ const router = useRouter(); // ⬅️
+
+ const form = useForm<FormValues>({
+ resolver: zodResolver(schema),
+ defaultValues: { category: "", inspectionItem: "", remarks: "" },
+ });
+
+ async function onSubmit(values: FormValues) {
+ setPending(true);
+ const res = await createGeneralEvaluation(values);
+ if (res.success) {
+ toast.success(res.message);
+ router.refresh(); // ❷ 새로고침
+
+ onSuccess?.();
+ setOpen(false);
+ form.reset();
+ } else {
+ toast.error(res.message);
+ }
+ setPending(false);
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={(v) => !pending && setOpen(v)}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <Plus className="size-4" /> 새 항목
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="sm:max-w-[480px]">
+ <DialogHeader>
+ <DialogTitle>새 정기평가 체크리스트</DialogTitle>
+ <DialogDescription>점검 항목을 추가합니다.</DialogDescription>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="category"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>카테고리</FormLabel>
+ <FormControl>
+ <Input placeholder="예: 안전" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="inspectionItem"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>점검 항목</FormLabel>
+ <FormControl>
+ <Input placeholder="예: 안전모 착용 여부" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="remarks"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>비고 (선택)</FormLabel>
+ <FormControl>
+ <Input placeholder="메모" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <DialogFooter>
+ <Button type="submit" disabled={pending}>
+ {pending ? "저장중..." : "저장"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file
diff --git a/lib/general-check-list/table/delete-check-lists-dialog.tsx b/lib/general-check-list/table/delete-check-lists-dialog.tsx
new file mode 100644
index 00000000..b4b4d648
--- /dev/null
+++ b/lib/general-check-list/table/delete-check-lists-dialog.tsx
@@ -0,0 +1,106 @@
+"use client";
+import * as React from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog";
+import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from "@/components/ui/drawer";
+import { Trash, Loader } from "lucide-react";
+import { useMediaQuery } from "@/hooks/use-media-query";
+import { deleteGeneralEvaluations } from "@/lib/general-check-list/service";
+import { toast } from "sonner";
+import { useRouter } from "next/navigation";
+
+export function DeleteEvaluationsDialog({
+ evaluations,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: {
+ evaluations: { id: number; serialNumber: string }[];
+ showTrigger?: boolean;
+ onSuccess?: () => void;
+ open?: boolean;
+ onOpenChange?: (v: boolean) => void;
+}) {
+ const [pending, startTransition] = React.useTransition();
+ const isDesktop = useMediaQuery("(min-width:640px)");
+ const router = useRouter();
+
+ const deleteText = evaluations.length === 1 ? "항목" : "항목들";
+
+ const handleDelete = () => {
+ startTransition(async () => {
+ const res = await deleteGeneralEvaluations(evaluations.map((e) => e.id));
+ if (res.success) {
+ toast.success(res.message);
+ router.refresh();
+ props.onOpenChange?.(false);
+ onSuccess?.();
+ } else {
+ toast.error(res.message);
+ }
+ });
+ };
+
+ const Content = (
+ <>
+ <DialogHeader>
+ <DialogTitle>삭제 확인</DialogTitle>
+ <DialogDescription>
+ 선택된 {evaluations.length}개의 {deleteText}을(를) 삭제합니다. 이 작업은 되돌릴 수 없습니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">취소</Button>
+ </DialogClose>
+ <Button variant="destructive" onClick={handleDelete} disabled={pending}>
+ {pending && <Loader className="mr-2 size-4 animate-spin" />} 삭제
+ </Button>
+ </DialogFooter>
+ </>
+ );
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger && (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="size-4 mr-2" /> 삭제({evaluations.length})
+ </Button>
+ </DialogTrigger>
+ )}
+ <DialogContent>{Content}</DialogContent>
+ </Dialog>
+ );
+ }
+
+ // Mobile Drawer
+ return (
+ <Drawer {...props}>
+ {showTrigger && (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="size-4 mr-2" /> 삭제({evaluations.length})
+ </Button>
+ </DrawerTrigger>
+ )}
+ <DrawerContent className="p-4 space-y-4">
+ <DrawerHeader>
+ <DrawerTitle>삭제 확인</DrawerTitle>
+ </DrawerHeader>
+ <DrawerDescription>
+ 선택된 {evaluations.length}개의 {deleteText}을(를) 삭제합니다. 이 작업은 되돌릴 수 없습니다.
+ </DrawerDescription>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">취소</Button>
+ </DrawerClose>
+ <Button variant="destructive" onClick={handleDelete} disabled={pending}>
+ {pending && <Loader className="mr-2 size-4 animate-spin" />} 삭제
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ );
+}
diff --git a/lib/general-check-list/table/general-check-list-table.tsx b/lib/general-check-list/table/general-check-list-table.tsx
new file mode 100644
index 00000000..34b39830
--- /dev/null
+++ b/lib/general-check-list/table/general-check-list-table.tsx
@@ -0,0 +1,63 @@
+"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 { DataTableFilterField, DataTableAdvancedFilterField } from "@/types/table";
+import { getGeneralEvaluationColumns } from "./general-check-table-columns";
+import { CreateEvaluationDialog } from "./add-check-list-dialog";
+import { DeleteEvaluationsDialog } from "./delete-check-lists-dialog";
+import { getGeneralEvaluations } from "@/lib/general-check-list/service";
+
+
+interface EsgEvaluationsTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getGeneralEvaluations>>,
+ ]
+ >
+ }
+
+export function GeneralEvaluationsTable({ promises }: EsgEvaluationsTableProps) {
+
+ const [{ data, pageCount }]= React.use(promises);
+ const columns = React.useMemo(() => getGeneralEvaluationColumns(), []);
+
+ // Filters (간단 예시)
+ const filterFields: DataTableFilterField<any>[] = [
+ { id: "serialNumber", label: "시리얼", placeholder: "시리얼 검색" },
+ { id: "category", label: "카테고리", placeholder: "카테고리" },
+ ];
+ const advFilterFields: DataTableAdvancedFilterField<any>[] = [
+ { id: "inspectionItem", label: "점검 항목", type: "text" },
+ { id: "remarks", label: "비고", type: "text" },
+ ];
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enableAdvancedFilter: true,
+ initialState: { sorting: [{ id: "createdAt", desc: true }], columnPinning: { right: ["actions"] } },
+ getRowId: (row) => row.id.toString(),
+ clearOnDefault: true,
+ shallow: false,
+ });
+
+ // Delete selected
+ const selectedRows = table.getFilteredSelectedRowModel().rows.map((r) => r.original);
+
+ return (
+ <div className="space-y-6">
+ <DataTable table={table} className="h-full">
+ <DataTableAdvancedToolbar table={table} filterFields={advFilterFields} shallow={false}>
+ <CreateEvaluationDialog />
+ {selectedRows.length > 0 && (
+ <DeleteEvaluationsDialog evaluations={selectedRows} />
+ )}
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </div>
+ );
+}
diff --git a/lib/general-check-list/table/general-check-table-columns.tsx b/lib/general-check-list/table/general-check-table-columns.tsx
new file mode 100644
index 00000000..c764686d
--- /dev/null
+++ b/lib/general-check-list/table/general-check-table-columns.tsx
@@ -0,0 +1,138 @@
+"use client";
+import * as React from "react";
+import { type ColumnDef } from "@tanstack/react-table";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Ellipsis, Pencil, Trash } from "lucide-react";
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
+import { DeleteEvaluationsDialog } from "./delete-check-lists-dialog";
+import { EditEvaluationSheet } from "./update-check-list-sheet";
+
+
+export interface GeneralEvaluationRow {
+ id: number;
+ serialNumber: string;
+ category: string;
+ inspectionItem: string;
+ remarks: string | null;
+ isActive: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export function getGeneralEvaluationColumns(): ColumnDef<GeneralEvaluationRow>[] {
+ return [
+ // Checkbox
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
+ onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
+ aria-label="select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(v) => row.toggleSelected(!!v)}
+ aria-label="select row"
+ className="translate-y-0.5"
+ />
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ },
+
+ // ░░░ Serial Number ░░░
+ {
+ accessorKey: "serialNumber",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="시리얼" />,
+ cell: ({ row }) => row.getValue("serialNumber"),
+ size: 120,
+ },
+
+ // ░░░ Category ░░░
+ {
+ accessorKey: "category",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="카테고리" />,
+ cell: ({ row }) => <Badge>{row.getValue("category")}</Badge>,
+ size: 120,
+ },
+
+ // ░░░ Inspection Item ░░░
+ {
+ accessorKey: "inspectionItem",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="점검 항목" />,
+ cell: ({ row }) => (
+ <div className="truncate max-w-[300px]" title={row.getValue<string>("inspectionItem")!}>
+ {row.getValue("inspectionItem") as string}
+ </div>
+ ),
+ size: 300,
+ },
+
+ // ░░░ Remarks ░░░
+ {
+ accessorKey: "remarks",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="비고" />,
+ cell: ({ row }) => row.getValue("remarks") ?? <span className="text-muted-foreground">-</span>,
+ size: 200,
+ },
+
+ // ░░░ 활성 ░░░
+ {
+ accessorKey: "isActive",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="활성" />,
+ cell: ({ row }) => (row.getValue("isActive") ? <Badge variant="default">활성</Badge> : <Badge variant="secondary">비활성</Badge>),
+ size: 80,
+ },
+
+ // ░░░ Actions ░░░
+ {
+ id: "actions",
+ enableHiding: false,
+ size: 40,
+ minSize:80,
+ cell: ({ row }) => {
+ const record = row.original;
+ const [openEdit, setOpenEdit] = React.useState(false);
+ const [openDelete, setOpenDelete] = React.useState(false);
+
+ return (
+ <>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setOpenEdit(true)}
+ aria-label="edit"
+ >
+ <Pencil className="size-4" />
+ </Button>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setOpenDelete(true)}
+ aria-label="delete"
+ >
+ <Trash className="size-4" />
+ </Button>
+
+ <EditEvaluationSheet open={openEdit} onOpenChange={setOpenEdit} evaluation={record} />
+ <DeleteEvaluationsDialog
+ open={openDelete}
+ onOpenChange={setOpenDelete}
+ evaluations={[record]}
+ showTrigger={false}
+ />
+ </>
+ );
+ },
+ },
+ ];
+}
diff --git a/lib/general-check-list/table/update-check-list-sheet.tsx b/lib/general-check-list/table/update-check-list-sheet.tsx
new file mode 100644
index 00000000..6c845465
--- /dev/null
+++ b/lib/general-check-list/table/update-check-list-sheet.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import * as React from "react";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { toast } from "sonner";
+import {
+ Sheet, // ⬅️ Drawer 대신 Sheet
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetFooter,
+ SheetClose,
+} from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import {
+ Form, FormField, FormItem, FormLabel,
+ FormControl, FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { updateGeneralEvaluation } from "@/lib/general-check-list/service";
+import { useRouter } from "next/navigation";
+
+const schema = z.object({
+ category: z.string(),
+ inspectionItem: z.string(),
+ remarks: z.string().optional(),
+ isActive: z.boolean().optional(),
+});
+
+type Values = z.infer<typeof schema>;
+
+export function EditEvaluationSheet({
+ open,
+ onOpenChange,
+ evaluation,
+ onSuccess,
+}: {
+ open: boolean;
+ onOpenChange: (v: boolean) => void;
+ evaluation: any;
+ onSuccess?: () => void;
+}) {
+ const [pending, setPending] = React.useState(false);
+ const router = useRouter(); // ⬅️
+
+ const form = useForm<Values>({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ category: evaluation.category,
+ inspectionItem: evaluation.inspectionItem,
+ remarks: evaluation.remarks ?? "",
+ isActive: evaluation.isActive,
+ },
+ });
+
+ async function onSubmit(values: Values) {
+ setPending(true);
+ const res = await updateGeneralEvaluation(evaluation.id, values);
+ setPending(false);
+
+ if (res.success) {
+ toast.success(res.message);
+ router.refresh();
+
+ onSuccess?.();
+ onOpenChange(false);
+ } else {
+ toast.error(res.message);
+ }
+ }
+
+ return (
+ <Sheet open={open} onOpenChange={(v) => !pending && onOpenChange(v)}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-lg">
+ <SheetHeader>
+ <SheetTitle>항목 수정 – {evaluation.serialNumber}</SheetTitle>
+ </SheetHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-4">
+ {/* 카테고리 */}
+ <FormField
+ control={form.control}
+ name="category"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>카테고리</FormLabel>
+ <FormControl>
+ <Input {...field} disabled={pending} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 점검 항목 */}
+ <FormField
+ control={form.control}
+ name="inspectionItem"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>점검 항목</FormLabel>
+ <FormControl>
+ <Input {...field} disabled={pending} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 비고 */}
+ <FormField
+ control={form.control}
+ name="remarks"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>비고</FormLabel>
+ <FormControl>
+ <Input {...field} disabled={pending} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 활성 체크 */}
+ <FormField
+ control={form.control}
+ name="isActive"
+ render={({ field }) => (
+ <FormItem className="flex items-center gap-2">
+ <FormLabel className="m-0">활성</FormLabel>
+ <FormControl>
+ <input
+ type="checkbox"
+ checked={field.value}
+ onChange={(e) => field.onChange(e.target.checked)}
+ disabled={pending}
+ />
+ </FormControl>
+ </FormItem>
+ )}
+ />
+
+ <SheetFooter className="pt-4">
+ <SheetClose asChild>
+ <Button variant="outline" disabled={pending}>
+ 취소
+ </Button>
+ </SheetClose>
+ <Button type="submit" disabled={pending}>
+ {pending ? "저장중..." : "저장"}
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ );
+}