summaryrefslogtreecommitdiff
path: root/lib/b-rfq/vendor-response/response-detail-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/vendor-response/response-detail-columns.tsx')
-rw-r--r--lib/b-rfq/vendor-response/response-detail-columns.tsx653
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