From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/candidates-table-floating-bar.tsx | 416 ++++++++++++--------- 1 file changed, 237 insertions(+), 179 deletions(-) (limited to 'lib/vendor-candidates/table/candidates-table-floating-bar.tsx') diff --git a/lib/vendor-candidates/table/candidates-table-floating-bar.tsx b/lib/vendor-candidates/table/candidates-table-floating-bar.tsx index 2696292d..baf4a583 100644 --- a/lib/vendor-candidates/table/candidates-table-floating-bar.tsx +++ b/lib/vendor-candidates/table/candidates-table-floating-bar.tsx @@ -30,37 +30,48 @@ import { 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 { vendorCandidates, VendorCandidates } from "@/db/schema/vendors" -import { bulkUpdateVendorCandidateStatus, removeCandidates, updateVendorCandidate } from "../service" +import { VendorCandidatesWithVendorInfo, vendorCandidates } from "@/db/schema/vendors" +import { + bulkUpdateVendorCandidateStatus, + removeCandidates, +} from "../service" +/** + * 테이블 상단/하단에 고정되는 Floating Bar + * 상태 일괄 변경, 초대, 삭제, Export 등을 수행 + */ interface CandidatesTableFloatingBarProps { - table: Table + table: Table } -export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloatingBarProps) { +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) - // Clear selection on Escape key press + // 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 state + // 공용 Confirm Dialog (ActionConfirmDialog) 제어 const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false) const [confirmProps, setConfirmProps] = React.useState<{ title: string @@ -69,25 +80,41 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati }>({ title: "", description: "", - onConfirm: () => { }, + onConfirm: () => {}, }) - // 1) "삭제" Confirm 열기 + /** + * 1) 삭제 버튼 클릭 시 Confirm Dialog 열기 + */ function handleDeleteConfirm() { setAction("delete") + setConfirmProps({ - title: `Delete ${rows.length} user${rows.length > 1 ? "s" : ""}?`, + title: `Delete ${rows.length} candidate${ + rows.length > 1 ? "s" : "" + }?`, description: "This action cannot be undone.", onConfirm: async () => { startTransition(async () => { - const { error } = await removeCandidates({ - ids: rows.map((row) => row.original.id), - }) + 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("Users deleted") + toast.success("Candidates deleted successfully") table.toggleAllRowsSelected(false) setConfirmDialogOpen(false) }) @@ -96,43 +123,71 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati setConfirmDialogOpen(true) } - // 2) 상태 업데이트 - function handleSelectStatus(newStatus: VendorCandidates["status"]) { + /** + * 2) 선택된 후보들의 상태 일괄 업데이트 + */ + function handleSelectStatus(newStatus: VendorCandidatesWithVendorInfo["status"]) { setAction("update-status") setConfirmProps({ - title: `Update ${rows.length} candidate${rows.length > 1 ? "s" : ""} with status: ${newStatus}?`, + 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) 초대하기 (INVITED 상태로 바꾸고 이메일 전송) + /** + * 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.", + 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 @@ -147,166 +202,168 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati } return ( - -
-
-
-
- - {rows.length} selected - - - - - - - -

Clear selection

- - Esc - -
-
-
- -
- {/* 초대하기 버튼 (새로 추가) */} - - - - - -

Send invitation emails

-
-
+ <> + {/* 선택된 row가 있을 때 표시되는 Floating Bar */} +
+
+ {/* 선택된 갯수 표시 + Clear selection 버튼 */} +
+ + {rows.length} selected + + + + + + + +

Clear selection

+ + Esc + +
+
+
- + - - - + + +

Send invitation emails

+
+
- startTransition(() => { - exportTableToExcel(table, { - excludeColumns: ["select", "actions"], - onlySelected: true, - }) - }) - }} - disabled={isPending} - > - {isPending && action === "export" ? ( -
@@ -318,7 +375,10 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati title={confirmProps.title} description={confirmProps.description} onConfirm={confirmProps.onConfirm} - isLoading={isPending && (action === "delete" || action === "update-status" || action === "invite")} + isLoading={ + isPending && + (action === "delete" || action === "update-status" || action === "invite") + } confirmLabel={ action === "delete" ? "Delete" @@ -328,10 +388,8 @@ export function VendorCandidateTableFloatingBar({ table }: CandidatesTableFloati ? "Invite" : "Confirm" } - confirmVariant={ - action === "delete" ? "destructive" : "default" - } + confirmVariant={action === "delete" ? "destructive" : "default"} /> - + ) -} \ No newline at end of file +} -- cgit v1.2.3