diff options
Diffstat (limited to 'lib/b-rfq/vendor-response/response-detail-columns.tsx')
| -rw-r--r-- | lib/b-rfq/vendor-response/response-detail-columns.tsx | 653 |
1 files changed, 0 insertions, 653 deletions
diff --git a/lib/b-rfq/vendor-response/response-detail-columns.tsx b/lib/b-rfq/vendor-response/response-detail-columns.tsx deleted file mode 100644 index bc27d103..00000000 --- a/lib/b-rfq/vendor-response/response-detail-columns.tsx +++ /dev/null @@ -1,653 +0,0 @@ -"use client" - -import * as React from "react" -import { ColumnDef } from "@tanstack/react-table" -import type { Row } from "@tanstack/react-table" -import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header" -import { formatDateTime } from "@/lib/utils" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - FileText, - Upload, - CheckCircle, - Clock, - AlertTriangle, - FileX, - Download, - AlertCircle, - RefreshCw, - Calendar, - MessageSquare, - GitBranch, - Ellipsis -} from "lucide-react" -import { cn } from "@/lib/utils" -import type { EnhancedVendorResponse } from "@/lib/b-rfq/service" -import { UploadResponseDialog } from "./upload-response-dialog" -import { CommentEditDialog } from "./comment-edit-dialog" -import { WaiveResponseDialog } from "./waive-response-dialog" -import { ResponseDetailSheet } from "./response-detail-sheet" - -export interface DataTableRowAction<TData> { - row: Row<TData> - type: 'upload' | 'waive' | 'edit' | 'detail' -} - -interface GetColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EnhancedVendorResponse> | null>> -} - -// 파일 다운로드 핸들러 -async function handleFileDownload( - filePath: string, - fileName: string, - type: "client" | "vendor" = "client", - id?: number -) { - try { - const params = new URLSearchParams({ - path: filePath, - type: type, - }); - - if (id) { - if (type === "client") { - params.append("revisionId", id.toString()); - } else { - params.append("responseFileId", id.toString()); - } - } - - const response = await fetch(`/api/rfq-attachments/download?${params.toString()}`); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Download failed: ${response.status}`); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = fileName; - document.body.appendChild(link); - link.click(); - - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - - } catch (error) { - console.error("❌ 파일 다운로드 실패:", error); - alert(`파일 다운로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); - } -} - -// 상태별 정보 반환 -function getEffectiveStatusInfo(effectiveStatus: string) { - switch (effectiveStatus) { - case "NOT_RESPONDED": - return { - icon: Clock, - label: "미응답", - variant: "outline" as const, - color: "text-orange-600" - }; - case "UP_TO_DATE": - return { - icon: CheckCircle, - label: "최신", - variant: "default" as const, - color: "text-green-600" - }; - case "VERSION_MISMATCH": - return { - icon: RefreshCw, - label: "업데이트 필요", - variant: "secondary" as const, - color: "text-blue-600" - }; - case "REVISION_REQUESTED": - return { - icon: AlertTriangle, - label: "수정요청", - variant: "secondary" as const, - color: "text-yellow-600" - }; - case "WAIVED": - return { - icon: FileX, - label: "포기", - variant: "outline" as const, - color: "text-gray-600" - }; - default: - return { - icon: FileText, - label: effectiveStatus, - variant: "outline" as const, - color: "text-gray-600" - }; - } -} - -// 파일명 컴포넌트 -function AttachmentFileNameCell({ revisions }: { - revisions: Array<{ - id: number; - originalFileName: string; - revisionNo: string; - isLatest: boolean; - filePath?: string; - fileSize: number; - createdAt: string; - revisionComment?: string; - }> -}) { - if (!revisions || revisions.length === 0) { - return <span className="text-muted-foreground">파일 없음</span>; - } - - const latestRevision = revisions.find(r => r.isLatest) || revisions[0]; - const hasMultipleRevisions = revisions.length > 1; - const canDownload = latestRevision.filePath; - - return ( - <div className="space-y-1"> - <div className="flex items-center gap-2"> - {canDownload ? ( - <button - onClick={() => handleFileDownload( - latestRevision.filePath!, - latestRevision.originalFileName, - "client", - latestRevision.id - )} - className="font-medium text-sm text-blue-600 hover:text-blue-800 hover:underline text-left max-w-64 truncate" - title={`${latestRevision.originalFileName} - 클릭하여 다운로드`} - > - {latestRevision.originalFileName} - </button> - ) : ( - <span className="font-medium text-sm text-muted-foreground max-w-64 truncate" title={latestRevision.originalFileName}> - {latestRevision.originalFileName} - </span> - )} - - {canDownload && ( - <Button - size="sm" - variant="ghost" - className="h-6 w-6 p-0" - onClick={() => handleFileDownload( - latestRevision.filePath!, - latestRevision.originalFileName, - "client", - latestRevision.id - )} - title="파일 다운로드" - > - <Download className="h-3 w-3" /> - </Button> - )} - - {hasMultipleRevisions && ( - <Badge variant="outline" className="text-xs"> - v{latestRevision.revisionNo} - </Badge> - )} - </div> - - {hasMultipleRevisions && ( - <div className="text-xs text-muted-foreground"> - 총 {revisions.length}개 리비전 - </div> - )} - </div> - ); -} - -// 리비전 비교 컴포넌트 -function RevisionComparisonCell({ response }: { response: EnhancedVendorResponse }) { - const isUpToDate = response.isVersionMatched; - const hasResponse = !!response.respondedRevision; - const versionLag = response.versionLag || 0; - - return ( - <div className="space-y-2"> - <div className="flex items-center gap-2"> - <span className="text-xs text-muted-foreground">발주처:</span> - <Badge variant="secondary" className="text-xs font-mono"> - {response.currentRevision} - </Badge> - </div> - <div className="flex items-center gap-2"> - <span className="text-xs text-muted-foreground">응답:</span> - {hasResponse ? ( - <Badge - variant={isUpToDate ? "default" : "outline"} - className={cn( - "text-xs font-mono", - !isUpToDate && "text-blue-600 border-blue-300" - )} - > - {response.respondedRevision} - </Badge> - ) : ( - <span className="text-xs text-muted-foreground">-</span> - )} - </div> - {hasResponse && !isUpToDate && versionLag > 0 && ( - <div className="flex items-center gap-1 text-xs text-blue-600"> - <AlertCircle className="h-3 w-3" /> - <span>{versionLag}버전 차이</span> - </div> - )} - {response.hasMultipleRevisions && ( - <div className="flex items-center gap-1 text-xs text-muted-foreground"> - <GitBranch className="h-3 w-3" /> - <span>다중 리비전</span> - </div> - )} - </div> - ); -} - -// 코멘트 표시 컴포넌트 -function CommentDisplayCell({ response }: { response: EnhancedVendorResponse }) { - const hasResponseComment = !!response.responseComment; - const hasVendorComment = !!response.vendorComment; - const hasRevisionRequestComment = !!response.revisionRequestComment; - const hasClientComment = !!response.attachment?.revisions?.find(r => r.revisionComment); - - const commentCount = [hasResponseComment, hasVendorComment, hasRevisionRequestComment, hasClientComment].filter(Boolean).length; - - if (commentCount === 0) { - return <span className="text-xs text-muted-foreground">-</span>; - } - - return ( - <div className="space-y-1"> - {hasResponseComment && ( - <div className="flex items-center gap-1"> - <div className="w-2 h-2 rounded-full bg-blue-500" title="벤더 응답 코멘트"></div> - <span className="text-xs text-blue-600 truncate max-w-32" title={response.responseComment}> - {response.responseComment} - </span> - </div> - )} - - {hasVendorComment && ( - <div className="flex items-center gap-1"> - <div className="w-2 h-2 rounded-full bg-green-500" title="벤더 내부 메모"></div> - <span className="text-xs text-green-600 truncate max-w-32" title={response.vendorComment}> - {response.vendorComment} - </span> - </div> - )} - - {hasRevisionRequestComment && ( - <div className="flex items-center gap-1"> - <div className="w-2 h-2 rounded-full bg-red-500" title="수정 요청 사유"></div> - <span className="text-xs text-red-600 truncate max-w-32" title={response.revisionRequestComment}> - {response.revisionRequestComment} - </span> - </div> - )} - - {hasClientComment && ( - <div className="flex items-center gap-1"> - <div className="w-2 h-2 rounded-full bg-orange-500" title="발주처 리비전 코멘트"></div> - <span className="text-xs text-orange-600 truncate max-w-32" - title={response.attachment?.revisions?.find(r => r.revisionComment)?.revisionComment}> - {response.attachment?.revisions?.find(r => r.revisionComment)?.revisionComment} - </span> - </div> - )} - - {/* <div className="text-xs text-muted-foreground text-center"> - {commentCount}개 - </div> */} - </div> - ); -} - -export function getColumns({ - setRowAction, -}: GetColumnsProps): ColumnDef<EnhancedVendorResponse>[] { - return [ - // 시리얼 번호 - 핀고정용 최소 너비 - { - accessorKey: "serialNo", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="시리얼" /> - ), - cell: ({ row }) => ( - <div className="font-mono text-sm">{row.getValue("serialNo")}</div> - ), - - meta: { - excelHeader: "시리얼", - paddingFactor: 0.8 - }, - }, - - // 분류 - 핀고정용 적절한 너비 - { - accessorKey: "attachmentType", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="분류" /> - ), - cell: ({ row }) => ( - <div className="space-y-1"> - <div className="font-medium text-sm">{row.getValue("attachmentType")}</div> - {row.original.attachmentDescription && ( - <div className="text-xs text-muted-foreground truncate max-w-32" - title={row.original.attachmentDescription}> - {row.original.attachmentDescription} - </div> - )} - </div> - ), - - meta: { - excelHeader: "분류", - paddingFactor: 1.0 - }, - }, - - // 파일명 - 가장 긴 텍스트를 위한 여유 공간 - { - id: "fileName", - header: "파일명", - cell: ({ row }) => ( - <AttachmentFileNameCell revisions={row.original.attachment?.revisions || []} /> - ), - - meta: { - paddingFactor: 1.5 - }, - }, - - // 상태 - 뱃지 크기 고려 - { - accessorKey: "effectiveStatus", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="상태" /> - ), - cell: ({ row }) => { - const statusInfo = getEffectiveStatusInfo(row.getValue("effectiveStatus")); - const StatusIcon = statusInfo.icon; - - return ( - <div className="space-y-1"> - <Badge variant={statusInfo.variant} className="flex items-center gap-1 w-fit"> - <StatusIcon className="h-3 w-3" /> - <span>{statusInfo.label}</span> - </Badge> - {row.original.needsUpdate && ( - <div className="text-xs text-blue-600 flex items-center gap-1"> - <RefreshCw className="h-3 w-3" /> - <span>업데이트 권장</span> - </div> - )} - </div> - ); - }, - - meta: { - excelHeader: "상태", - paddingFactor: 1.2 - }, - }, - - // 리비전 현황 - 복합 정보로 넓은 공간 필요 - { - id: "revisionStatus", - header: "리비전 현황", - cell: ({ row }) => <RevisionComparisonCell response={row.original} />, - - meta: { - paddingFactor: 1.3 - }, - }, - - // 요청일 - 날짜 형식 고정 - { - accessorKey: "requestedAt", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="요청일" /> - ), - cell: ({ row }) => ( - <div className="text-sm flex items-center gap-1"> - <Calendar className="h-3 w-3 text-muted-foreground" /> - <span className="whitespace-nowrap">{formatDateTime(new Date(row.getValue("requestedAt")))}</span> - </div> - ), - - meta: { - excelHeader: "요청일", - paddingFactor: 0.9 - }, - }, - - // 응답일 - 날짜 형식 고정 - { - accessorKey: "respondedAt", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="응답일" /> - ), - cell: ({ row }) => ( - <div className="text-sm"> - <span className="whitespace-nowrap"> - {row.getValue("respondedAt") - ? formatDateTime(new Date(row.getValue("respondedAt"))) - : "-" - } - </span> - </div> - ), - meta: { - excelHeader: "응답일", - paddingFactor: 0.9 - }, - }, - - // 응답파일 - 작은 공간 - { - accessorKey: "totalResponseFiles", - header: ({ column }) => ( - <ClientDataTableColumnHeaderSimple column={column} title="응답파일" /> - ), - cell: ({ row }) => ( - <div className="text-center"> - <div className="text-sm font-medium"> - {row.getValue("totalResponseFiles")}개 - </div> - {row.original.latestResponseFileName && ( - <div className="text-xs text-muted-foreground truncate max-w-20" - title={row.original.latestResponseFileName}> - {row.original.latestResponseFileName} - </div> - )} - </div> - ), - meta: { - excelHeader: "응답파일", - paddingFactor: 0.8 - }, - }, - - // 코멘트 - 가변 텍스트 길이 - { - id: "comments", - header: "코멘트", - cell: ({ row }) => <CommentDisplayCell response={row.original} />, - // size: 180, - meta: { - paddingFactor: 1.4 - }, - }, - - // 진행도 - 중간 크기 - { - id: "progress", - header: "진행도", - cell: ({ row }) => ( - <div className="space-y-1 text-center"> - {row.original.hasMultipleRevisions && ( - <Badge variant="outline" className="text-xs"> - 다중 리비전 - </Badge> - )} - {row.original.versionLag !== undefined && row.original.versionLag > 0 && ( - <div className="text-xs text-blue-600 whitespace-nowrap"> - {row.original.versionLag}버전 차이 - </div> - )} - </div> - ), - // size: 100, - meta: { - paddingFactor: 1.1 - }, - }, - -{ - id: "actions", - enableHiding: false, - cell: function Cell({ row }) { - const response = row.original; - - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - aria-label="Open menu" - variant="ghost" - className="flex size-8 p-0 data-[state=open]:bg-muted" - > - <Ellipsis className="size-4" aria-hidden="true" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-56"> - {/* 상태별 주요 액션들 */} - {response.effectiveStatus === "NOT_RESPONDED" && ( - <> - <DropdownMenuItem asChild> - <UploadResponseDialog - responseId={response.responseId} - attachmentType={response.attachmentType} - serialNo={response.serialNo} - currentRevision={response.currentRevision} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <Upload className="size-4 mr-2" /> - 업로드 - </div> - } - /> - </DropdownMenuItem> - <DropdownMenuItem asChild> - <WaiveResponseDialog - responseId={response.responseId} - attachmentType={response.attachmentType} - serialNo={response.serialNo} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <FileX className="size-4 mr-2" /> - 포기 - </div> - } - /> - </DropdownMenuItem> - </> - )} - - {response.effectiveStatus === "REVISION_REQUESTED" && ( - <DropdownMenuItem asChild> - <UploadResponseDialog - responseId={response.responseId} - attachmentType={response.attachmentType} - serialNo={response.serialNo} - currentRevision={response.currentRevision} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <FileText className="size-4 mr-2" /> - 수정 - </div> - } - /> - </DropdownMenuItem> - )} - - {response.effectiveStatus === "VERSION_MISMATCH" && ( - <DropdownMenuItem asChild> - <UploadResponseDialog - responseId={response.responseId} - attachmentType={response.attachmentType} - serialNo={response.serialNo} - currentRevision={response.currentRevision} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <RefreshCw className="size-4 mr-2" /> - 업데이트 - </div> - } - /> - </DropdownMenuItem> - )} - - {/* 구분선 - 주요 액션과 보조 액션 분리 */} - {(response.effectiveStatus === "NOT_RESPONDED" || - response.effectiveStatus === "REVISION_REQUESTED" || - response.effectiveStatus === "VERSION_MISMATCH") && - response.effectiveStatus !== "WAIVED" && ( - <DropdownMenuSeparator /> - )} - - {/* 공통 액션들 */} - {response.effectiveStatus !== "WAIVED" && ( - <DropdownMenuItem asChild> - <CommentEditDialog - responseId={response.responseId} - currentResponseComment={response.responseComment || ""} - currentVendorComment={response.vendorComment || ""} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <MessageSquare className="size-4 mr-2" /> - 코멘트 편집 - </div> - } - /> - </DropdownMenuItem> - )} - - <DropdownMenuItem asChild> - <ResponseDetailSheet - response={response} - trigger={ - <div className="flex items-center w-full cursor-pointer p-2"> - <FileText className="size-4 mr-2" /> - 상세보기 - </div> - } - /> - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - - ) - }, - size: 40, -} - - ] -}
\ No newline at end of file |
