From a75541e1a1aea596bfca2a435f39133b9b72f193 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 23 Jun 2025 09:00:56 +0000 Subject: (최겸) 기술영업 벤더 후보관리 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/candidates-table-floating-bar.tsx | 395 +++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx (limited to 'lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx') diff --git a/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx b/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx new file mode 100644 index 00000000..baf4a583 --- /dev/null +++ b/lib/tech-vendor-candidates/table/candidates-table-floating-bar.tsx @@ -0,0 +1,395 @@ +"use client" + +import * as React from "react" +import { SelectTrigger } from "@radix-ui/react-select" +import { type Table } from "@tanstack/react-table" +import { + ArrowUp, + CheckCircle2, + Download, + Loader, + Trash2, + X, + Mail, +} from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { Portal } from "@/components/ui/portal" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Kbd } from "@/components/kbd" +import { useSession } from "next-auth/react" // next-auth 세션 훅 + +import { ActionConfirmDialog } from "@/components/ui/action-dialog" +import { VendorCandidatesWithVendorInfo, vendorCandidates } from "@/db/schema/vendors" +import { + bulkUpdateVendorCandidateStatus, + removeCandidates, +} from "../service" + +/** + * 테이블 상단/하단에 고정되는 Floating Bar + * 상태 일괄 변경, 초대, 삭제, Export 등을 수행 + */ +interface CandidatesTableFloatingBarProps { + table: Table +} + +export function VendorCandidateTableFloatingBar({ + table, +}: CandidatesTableFloatingBarProps) { + const rows = table.getFilteredSelectedRowModel().rows + const { data: session, status } = useSession() + + // React 18의 startTransition 사용 (isPending으로 트랜지션 상태 확인) + const [isPending, startTransition] = React.useTransition() + const [action, setAction] = React.useState< + "update-status" | "export" | "delete" | "invite" + >() + const [popoverOpen, setPopoverOpen] = React.useState(false) + + // ESC 키로 selection 해제 + React.useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + table.toggleAllRowsSelected(false) + } + } + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [table]) + + // 공용 Confirm Dialog (ActionConfirmDialog) 제어 + const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false) + const [confirmProps, setConfirmProps] = React.useState<{ + title: string + description?: string + onConfirm: () => Promise | void + }>({ + title: "", + description: "", + onConfirm: () => {}, + }) + + /** + * 1) 삭제 버튼 클릭 시 Confirm Dialog 열기 + */ + function handleDeleteConfirm() { + setAction("delete") + + setConfirmProps({ + title: `Delete ${rows.length} candidate${ + rows.length > 1 ? "s" : "" + }?`, + description: "This action cannot be undone.", + onConfirm: async () => { + startTransition(async () => { + if (!session?.user?.id) { + toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") + return + } + const userId = Number(session.user.id) + + // removeCandidates 호출 시 userId를 넘긴다고 가정 + const { error } = await removeCandidates( + { + ids: rows.map((row) => row.original.id), + }, + userId + ) + + if (error) { + toast.error(error) + return + } + toast.success("Candidates deleted successfully") + table.toggleAllRowsSelected(false) + setConfirmDialogOpen(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + /** + * 2) 선택된 후보들의 상태 일괄 업데이트 + */ + function handleSelectStatus(newStatus: VendorCandidatesWithVendorInfo["status"]) { + setAction("update-status") + + setConfirmProps({ + title: `Update ${rows.length} candidate${ + rows.length > 1 ? "s" : "" + } with status: ${newStatus}?`, + description: "This action will override their current status.", + onConfirm: async () => { + startTransition(async () => { + if (!session?.user?.id) { + toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") + return + } + const userId = Number(session.user.id) + + const { error } = await bulkUpdateVendorCandidateStatus({ + ids: rows.map((row) => row.original.id), + status: newStatus, + userId, + comment: `Bulk status update to ${newStatus}`, + }) + + if (error) { + toast.error(error) + return + } + toast.success("Candidates updated") + setConfirmDialogOpen(false) + table.toggleAllRowsSelected(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + /** + * 3) 초대하기 (status = "INVITED" + 이메일 발송) + */ + function handleInvite() { + setAction("invite") + setConfirmProps({ + title: `Invite ${rows.length} candidate${ + rows.length > 1 ? "s" : "" + }?`, + description: + "This will change their status to INVITED and send invitation emails.", + onConfirm: async () => { + startTransition(async () => { + if (!session?.user?.id) { + toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") + return + } + const userId = Number(session.user.id) + + const { error } = await bulkUpdateVendorCandidateStatus({ + ids: rows.map((row) => row.original.id), + status: "INVITED", + userId, + comment: "Bulk invite action", + }) + + if (error) { + toast.error(error) + return + } + toast.success("Invitation emails sent successfully") + table.toggleAllRowsSelected(false) + setConfirmDialogOpen(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + return ( + <> + {/* 선택된 row가 있을 때 표시되는 Floating Bar */} +
+
+ {/* 선택된 갯수 표시 + Clear selection 버튼 */} +
+ + {rows.length} selected + + + + + + + +

Clear selection

+ + Esc + +
+
+
+ + + + {/* 우측 액션들: 초대, 상태변경, Export, 삭제 */} +
+ {/* 초대하기 */} + + + + + +

Send invitation emails

+
+
+ + {/* 상태 업데이트 (Select) */} + + + {/* Export 버튼 */} + + + + + +

Export candidates

+
+
+ + {/* 삭제 버튼 */} + + + + + +

Delete candidates

+
+
+
+
+
+ + {/* 공용 Confirm Dialog */} + + + ) +} -- cgit v1.2.3