"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 { row: Row type: 'upload' | 'waive' | 'edit' | 'detail' } interface GetColumnsProps { setRowAction: React.Dispatch | 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 파일 없음; } const latestRevision = revisions.find(r => r.isLatest) || revisions[0]; const hasMultipleRevisions = revisions.length > 1; const canDownload = latestRevision.filePath; return (
{canDownload ? ( ) : ( {latestRevision.originalFileName} )} {canDownload && ( )} {hasMultipleRevisions && ( v{latestRevision.revisionNo} )}
{hasMultipleRevisions && (
총 {revisions.length}개 리비전
)}
); } // 리비전 비교 컴포넌트 function RevisionComparisonCell({ response }: { response: EnhancedVendorResponse }) { const isUpToDate = response.isVersionMatched; const hasResponse = !!response.respondedRevision; const versionLag = response.versionLag || 0; return (
발주처: {response.currentRevision}
응답: {hasResponse ? ( {response.respondedRevision} ) : ( - )}
{hasResponse && !isUpToDate && versionLag > 0 && (
{versionLag}버전 차이
)} {response.hasMultipleRevisions && (
다중 리비전
)}
); } // 코멘트 표시 컴포넌트 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 -; } return (
{hasResponseComment && (
{response.responseComment}
)} {hasVendorComment && (
{response.vendorComment}
)} {hasRevisionRequestComment && (
{response.revisionRequestComment}
)} {hasClientComment && (
r.revisionComment)?.revisionComment}> {response.attachment?.revisions?.find(r => r.revisionComment)?.revisionComment}
)} {/*
{commentCount}개
*/}
); } export function getColumns({ setRowAction, }: GetColumnsProps): ColumnDef[] { return [ // 시리얼 번호 - 핀고정용 최소 너비 { accessorKey: "serialNo", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("serialNo")}
), meta: { excelHeader: "시리얼", paddingFactor: 0.8 }, }, // 분류 - 핀고정용 적절한 너비 { accessorKey: "attachmentType", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("attachmentType")}
{row.original.attachmentDescription && (
{row.original.attachmentDescription}
)}
), meta: { excelHeader: "분류", paddingFactor: 1.0 }, }, // 파일명 - 가장 긴 텍스트를 위한 여유 공간 { id: "fileName", header: "파일명", cell: ({ row }) => ( ), meta: { paddingFactor: 1.5 }, }, // 상태 - 뱃지 크기 고려 { accessorKey: "effectiveStatus", header: ({ column }) => ( ), cell: ({ row }) => { const statusInfo = getEffectiveStatusInfo(row.getValue("effectiveStatus")); const StatusIcon = statusInfo.icon; return (
{statusInfo.label} {row.original.needsUpdate && (
업데이트 권장
)}
); }, meta: { excelHeader: "상태", paddingFactor: 1.2 }, }, // 리비전 현황 - 복합 정보로 넓은 공간 필요 { id: "revisionStatus", header: "리비전 현황", cell: ({ row }) => , meta: { paddingFactor: 1.3 }, }, // 요청일 - 날짜 형식 고정 { accessorKey: "requestedAt", header: ({ column }) => ( ), cell: ({ row }) => (
{formatDateTime(new Date(row.getValue("requestedAt")))}
), meta: { excelHeader: "요청일", paddingFactor: 0.9 }, }, // 응답일 - 날짜 형식 고정 { accessorKey: "respondedAt", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("respondedAt") ? formatDateTime(new Date(row.getValue("respondedAt"))) : "-" }
), meta: { excelHeader: "응답일", paddingFactor: 0.9 }, }, // 응답파일 - 작은 공간 { accessorKey: "totalResponseFiles", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("totalResponseFiles")}개
{row.original.latestResponseFileName && (
{row.original.latestResponseFileName}
)}
), meta: { excelHeader: "응답파일", paddingFactor: 0.8 }, }, // 코멘트 - 가변 텍스트 길이 { id: "comments", header: "코멘트", cell: ({ row }) => , // size: 180, meta: { paddingFactor: 1.4 }, }, // 진행도 - 중간 크기 { id: "progress", header: "진행도", cell: ({ row }) => (
{row.original.hasMultipleRevisions && ( 다중 리비전 )} {row.original.versionLag !== undefined && row.original.versionLag > 0 && (
{row.original.versionLag}버전 차이
)}
), // size: 100, meta: { paddingFactor: 1.1 }, }, { id: "actions", enableHiding: false, cell: function Cell({ row }) { const response = row.original; return ( {/* 상태별 주요 액션들 */} {response.effectiveStatus === "NOT_RESPONDED" && ( <> 업로드 } /> 포기 } /> )} {response.effectiveStatus === "REVISION_REQUESTED" && ( 수정 } /> )} {response.effectiveStatus === "VERSION_MISMATCH" && ( 업데이트 } /> )} {/* 구분선 - 주요 액션과 보조 액션 분리 */} {(response.effectiveStatus === "NOT_RESPONDED" || response.effectiveStatus === "REVISION_REQUESTED" || response.effectiveStatus === "VERSION_MISMATCH") && response.effectiveStatus !== "WAIVED" && ( )} {/* 공통 액션들 */} {response.effectiveStatus !== "WAIVED" && ( 코멘트 편집 } /> )} 상세보기 } /> ) }, size: 40, } ] }