diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
| commit | 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch) | |
| tree | f406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/qna/table/qna-table-columns.tsx | |
| parent | 6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff) | |
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/qna/table/qna-table-columns.tsx')
| -rw-r--r-- | lib/qna/table/qna-table-columns.tsx | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/lib/qna/table/qna-table-columns.tsx b/lib/qna/table/qna-table-columns.tsx new file mode 100644 index 00000000..01431e35 --- /dev/null +++ b/lib/qna/table/qna-table-columns.tsx @@ -0,0 +1,325 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { useRouter } from "next/navigation" +import { + MoreHorizontal, + Eye, + Edit, + Trash2, + MessageSquare, + MessageCircle, + Clock, + Building2, + User, + CheckCircle2, + AlertCircle, + TrendingUp +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" + +import { formatDate } from "@/lib/utils" +import { QnaViewSelect } from "@/db/schema" +import type { DataTableRowAction } from "@/types/table" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + + +type NextRouter = ReturnType<typeof useRouter>; + +interface GetColumnsOptions { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<QnaViewSelect> | null>> + router: NextRouter; + currentUserId?: number | string; // ← 추가 + +} + +export function getColumns({ setRowAction, router, currentUserId }: GetColumnsOptions): ColumnDef<QnaViewSelect>[] { + return [ + // 선택 체크박스 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="모두 선택" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="행 선택" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // 제목 (클릭 시 상세 페이지 이동) + { + accessorKey: "title", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="제목" /> + ), + cell: ({ row }) => { + const qna = row.original + + return ( + <div className="flex flex-col gap-1"> + <Button + variant="link" + className="h-auto p-0 text-left justify-start font-medium text-foreground hover:text-primary" + onClick={() => router.push(`/evcp/qna/${qna.id}`)} + > + <span className="line-clamp-2 max-w-[300px]"> + {qna.title} + </span> + </Button> + + {/* 상태 배지들 */} + <div className="flex items-center gap-1 flex-wrap"> + {qna.hasAnswers && ( + <Badge variant="secondary" className="text-xs"> + <CheckCircle2 className="w-3 h-3 mr-1" /> + 답변됨 + </Badge> + )} + {!qna.hasAnswers && ( + <Badge variant="outline" className="text-xs"> + <AlertCircle className="w-3 h-3 mr-1" /> + 답변 대기 + </Badge> + )} + {qna.isPopular && ( + <Badge variant="default" className="text-xs"> + <TrendingUp className="w-3 h-3 mr-1" /> + 인기 + </Badge> + )} + </div> + </div> + ) + }, + enableSorting: true, + enableHiding: false, + }, + + // 작성자 정보 + { + accessorKey: "authorName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="작성자" /> + ), + cell: ({ row }) => { + const qna = row.original + + return ( + <div className="flex items-center gap-2"> + <Avatar className="h-8 w-8"> + <AvatarImage src={qna.authorImageUrl || undefined} /> + <AvatarFallback> + {qna.authorName?.slice(0, 2) || "??"} + </AvatarFallback> + </Avatar> + <div className="flex flex-col"> + <span className="font-medium text-sm">{qna.authorName}</span> + <span className="text-xs text-muted-foreground"> + {qna.authorEmail} + </span> + </div> + </div> + ) + }, + enableSorting: true, + }, + + // 회사 정보 + { + accessorKey: "companyName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple + column={column} + title="회사" + /> + ), + cell: ({ row }) => { + const qna = row.original + + return ( + <div className="flex flex-col gap-1"> + <span className="font-medium text-sm"> + {qna.companyName || "미지정"} + </span> + {qna.vendorType && ( + <span + className="text-xs w-fit" + > + {qna.vendorType === "vendor" ? "일반 벤더" : "기술 벤더"} + </span> + )} + </div> + ) + }, + enableSorting: true, + }, + + // 도메인 + { + accessorKey: "category", + header: "카테고리", + cell: ({ row }) => { + const domain = row.original.category + return ( + <Badge variant="outline" className="text-xs"> + {domain} + </Badge> + ) + }, + enableSorting: true, + }, + + // 답변/댓글 통계 + { + id: "statistics", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="활동" /> + ), + cell: ({ row }) => { + const qna = row.original + + return ( + <div className="flex items-center gap-3"> + <Tooltip> + <TooltipTrigger asChild> + <div className="flex items-center gap-1 text-sm"> + <MessageSquare className="h-4 w-4 text-blue-500" /> + <span className="font-medium">{qna.totalAnswers || 0}</span> + </div> + </TooltipTrigger> + <TooltipContent>답변 수</TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <div className="flex items-center gap-1 text-sm"> + <MessageCircle className="h-4 w-4 text-green-500" /> + <span className="font-medium">{qna.totalComments || 0}</span> + </div> + </TooltipTrigger> + <TooltipContent>댓글 수</TooltipContent> + </Tooltip> + </div> + ) + }, + enableSorting: false, + }, + + // 작성일 + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="작성일" /> + ), + cell: ({ row }) => ( + <div className="text-sm"> + {formatDate(row.original.createdAt)} + </div> + ), + enableSorting: true, + }, + + // 최근 활동 + { + accessorKey: "lastActivityAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple + column={column} + title="최근 활동" + /> + ), + cell: ({ row }) => { + const lastActivity = row.original.lastActivityAt + + return ( + <div className="text-sm"> + {lastActivity ? formatDate(lastActivity) : "없음"} + </div> + ) + }, + enableSorting: true, + }, + + // 액션 메뉴 + { + id: "actions", + cell: ({ row }) => { + const qna = row.original + const isAuthor = qna.author === currentUserId + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="메뉴 열기" + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <MoreHorizontal className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[160px]"> + {/* ───────── 공통 : 상세 보기 ───────── */} + <DropdownMenuItem onClick={() => router.push(`/evcp/qna/${qna.id}`)}> + <Eye className="mr-2 h-4 w-4" /> + 상세보기 + </DropdownMenuItem> + + {/* ───────── 본인 글일 때만 노출 ───────── */} + {isAuthor && ( + <> + <DropdownMenuItem onClick={() => setRowAction({ type: "update", row })}> + <Edit className="mr-2 h-4 w-4" /> + 수정 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ type: "delete", row })} + className="text-destructive focus:text-destructive" + > + <Trash2 className="mr-2 h-4 w-4" /> + 삭제 + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + </> + )} + </DropdownMenuContent> + </DropdownMenu> + ) + }, + enableSorting: false, + enableHiding: false, + }, + ] +}
\ No newline at end of file |
