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