diff options
Diffstat (limited to 'lib/b-rfq/vendor-response/response-detail-sheet.tsx')
| -rw-r--r-- | lib/b-rfq/vendor-response/response-detail-sheet.tsx | 358 |
1 files changed, 358 insertions, 0 deletions
diff --git a/lib/b-rfq/vendor-response/response-detail-sheet.tsx b/lib/b-rfq/vendor-response/response-detail-sheet.tsx new file mode 100644 index 00000000..da7f9b01 --- /dev/null +++ b/lib/b-rfq/vendor-response/response-detail-sheet.tsx @@ -0,0 +1,358 @@ +// components/rfq/response-detail-sheet.tsx +"use client"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + FileText, + Upload, + Download, + AlertCircle, + MessageSquare, + FileCheck, + Eye +} from "lucide-react"; +import { formatDateTime, formatFileSize } from "@/lib/utils"; +import { cn } from "@/lib/utils"; +import type { EnhancedVendorResponse } from "@/lib/b-rfq/service"; + +// 파일 다운로드 핸들러 (API 사용) +async function handleFileDownload( + filePath: string, + fileName: string, + type: "client" | "vendor" = "client", + id?: number +) { + try { + const params = new URLSearchParams({ + path: filePath, + type: type, + }); + + // ID가 있으면 추가 + 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}`); + } + + // Blob으로 파일 데이터 받기 + const blob = await response.blob(); + + // 임시 URL 생성하여 다운로드 + 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); + + console.log("✅ 파일 다운로드 성공:", fileName); + + } catch (error) { + console.error("❌ 파일 다운로드 실패:", error); + + // 사용자에게 에러 알림 (토스트나 알럿으로 대체 가능) + alert(`파일 다운로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } +} + +// 효과적인 상태별 아이콘 및 색상 +function getEffectiveStatusInfo(effectiveStatus: string) { + switch (effectiveStatus) { + case "NOT_RESPONDED": + return { + label: "미응답", + variant: "outline" as const + }; + case "UP_TO_DATE": + return { + label: "최신", + variant: "default" as const + }; + case "VERSION_MISMATCH": + return { + label: "업데이트 필요", + variant: "secondary" as const + }; + case "REVISION_REQUESTED": + return { + label: "수정요청", + variant: "secondary" as const + }; + case "WAIVED": + return { + label: "포기", + variant: "outline" as const + }; + default: + return { + label: effectiveStatus, + variant: "outline" as const + }; + } +} + +interface ResponseDetailSheetProps { + response: EnhancedVendorResponse; + trigger?: React.ReactNode; +} + +export function ResponseDetailSheet({ response, trigger }: ResponseDetailSheetProps) { + const hasMultipleRevisions = response.attachment?.revisions && response.attachment.revisions.length > 1; + const hasResponseFiles = response.responseAttachments && response.responseAttachments.length > 0; + + return ( + <Sheet> + <SheetTrigger asChild> + {trigger || ( + <Button size="sm" variant="ghost"> + <Eye className="h-3 w-3 mr-1" /> + 상세 + </Button> + )} + </SheetTrigger> + <SheetContent side="right" className="w-[600px] sm:w-[800px] overflow-y-auto"> + <SheetHeader> + <SheetTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 상세 정보 - {response.serialNo} + </SheetTitle> + <SheetDescription> + {response.attachmentType} • {response.attachment?.revisions?.[0]?.originalFileName} + </SheetDescription> + </SheetHeader> + + <div className="space-y-6 mt-6"> + {/* 기본 정보 */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold flex items-center gap-2"> + <AlertCircle className="h-4 w-4" /> + 기본 정보 + </h3> + <div className="grid grid-cols-2 gap-4 p-4 bg-muted/30 rounded-lg"> + <div> + <div className="text-sm text-muted-foreground">상태</div> + <div className="font-medium">{getEffectiveStatusInfo(response.effectiveStatus).label}</div> + </div> + <div> + <div className="text-sm text-muted-foreground">현재 리비전</div> + <div className="font-medium">{response.currentRevision}</div> + </div> + <div> + <div className="text-sm text-muted-foreground">응답 리비전</div> + <div className="font-medium">{response.respondedRevision || "-"}</div> + </div> + <div> + <div className="text-sm text-muted-foreground">응답일</div> + <div className="font-medium"> + {response.respondedAt ? formatDateTime(new Date(response.respondedAt)) : "-"} + </div> + </div> + <div> + <div className="text-sm text-muted-foreground">요청일</div> + <div className="font-medium"> + {formatDateTime(new Date(response.requestedAt))} + </div> + </div> + <div> + <div className="text-sm text-muted-foreground">응답 파일 수</div> + <div className="font-medium">{response.totalResponseFiles}개</div> + </div> + </div> + </div> + + {/* 코멘트 정보 */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold flex items-center gap-2"> + <MessageSquare className="h-4 w-4" /> + 코멘트 + </h3> + <div className="space-y-3"> + {response.responseComment && ( + <div className="p-3 border-l-4 border-blue-500 bg-blue-50"> + <div className="text-sm font-medium text-blue-700 mb-1">발주처 응답 코멘트</div> + <div className="text-sm">{response.responseComment}</div> + </div> + )} + {response.vendorComment && ( + <div className="p-3 border-l-4 border-green-500 bg-green-50"> + <div className="text-sm font-medium text-green-700 mb-1">내부 메모</div> + <div className="text-sm">{response.vendorComment}</div> + </div> + )} + {response.attachment?.revisions?.find(r => r.revisionComment) && ( + <div className="p-3 border-l-4 border-orange-500 bg-orange-50"> + <div className="text-sm font-medium text-orange-700 mb-1">발주처 요청 사항</div> + <div className="text-sm"> + {response.attachment.revisions.find(r => r.revisionComment)?.revisionComment} + </div> + </div> + )} + {!response.responseComment && !response.vendorComment && !response.attachment?.revisions?.find(r => r.revisionComment) && ( + <div className="text-center text-muted-foreground py-4 bg-muted/20 rounded-lg"> + 코멘트가 없습니다. + </div> + )} + </div> + </div> + + {/* 발주처 리비전 히스토리 */} + {hasMultipleRevisions && ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold flex items-center gap-2"> + <FileCheck className="h-4 w-4" /> + 발주처 리비전 히스토리 ({response.attachment!.revisions.length}개) + </h3> + <div className="space-y-3"> + {response.attachment!.revisions + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .map((revision) => ( + <div + key={revision.id} + className={cn( + "flex items-center justify-between p-4 rounded-lg border", + revision.isLatest ? "bg-blue-50 border-blue-200" : "bg-white" + )} + > + <div className="flex items-center gap-3 flex-1"> + <Badge variant={revision.isLatest ? "default" : "outline"}> + {revision.revisionNo} + </Badge> + <div className="flex-1"> + <div className="font-medium text-sm">{revision.originalFileName}</div> + <div className="text-xs text-muted-foreground"> + {formatFileSize(revision.fileSize)} • {formatDateTime(new Date(revision.createdAt))} + </div> + {revision.revisionComment && ( + <div className="text-xs text-muted-foreground mt-1 italic"> + "{revision.revisionComment}" + </div> + )} + </div> + </div> + + <div className="flex items-center gap-2"> + {revision.isLatest && ( + <Badge variant="secondary" className="text-xs">최신</Badge> + )} + {revision.revisionNo === response.respondedRevision && ( + <Badge variant="outline" className="text-xs text-green-600 border-green-300"> + 응답됨 + </Badge> + )} + <Button + size="sm" + variant="ghost" + onClick={() => { + if (revision.filePath) { + handleFileDownload( + revision.filePath, + revision.originalFileName, + "client", + revision.id + ); + } + }} + disabled={!revision.filePath} + title="파일 다운로드" + > + <Download className="h-4 w-4" /> + </Button> + </div> + </div> + ))} + </div> + </div> + )} + + {/* 벤더 응답 파일들 */} + {hasResponseFiles && ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold flex items-center gap-2"> + <Upload className="h-4 w-4" /> + 벤더 응답 파일들 ({response.totalResponseFiles}개) + </h3> + <div className="space-y-3"> + {response.responseAttachments! + .sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()) + .map((file) => ( + <div key={file.id} className="flex items-center justify-between p-4 rounded-lg border bg-green-50 border-green-200"> + <div className="flex items-center gap-3 flex-1"> + <Badge variant="outline" className="bg-green-100"> + 파일 #{file.fileSequence} + </Badge> + <div className="flex-1"> + <div className="font-medium text-sm">{file.originalFileName}</div> + <div className="text-xs text-muted-foreground"> + {formatFileSize(file.fileSize)} • {formatDateTime(new Date(file.uploadedAt))} + </div> + {file.description && ( + <div className="text-xs text-muted-foreground mt-1 italic"> + "{file.description}" + </div> + )} + </div> + </div> + + <div className="flex items-center gap-2"> + {file.isLatestResponseFile && ( + <Badge variant="secondary" className="text-xs">최신</Badge> + )} + <Button + size="sm" + variant="ghost" + onClick={() => { + if (file.filePath) { + handleFileDownload( + file.filePath, + file.originalFileName, + "vendor", + file.id + ); + } + }} + disabled={!file.filePath} + title="파일 다운로드" + > + <Download className="h-4 w-4" /> + </Button> + </div> + </div> + ))} + </div> + </div> + )} + + {!hasMultipleRevisions && !hasResponseFiles && ( + <div className="text-center text-muted-foreground py-8 bg-muted/20 rounded-lg"> + <FileText className="h-8 w-8 mx-auto mb-2 opacity-50" /> + <p>추가 파일이나 리비전 정보가 없습니다.</p> + </div> + )} + </div> + </SheetContent> + </Sheet> + ); +}
\ No newline at end of file |
