From 95866a13ba4e1c235373834460aa284b763fe0d9 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 23 Jun 2025 09:03:29 +0000
Subject: (최겸) 기술영업 RFQ 개발(0620 요구사항, 첨부파일, REV 등)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../tech-sales-quotation-attachments-sheet.tsx     | 231 +++++++++++++++++++++
 1 file changed, 231 insertions(+)
 create mode 100644 lib/techsales-rfq/table/tech-sales-quotation-attachments-sheet.tsx

(limited to 'lib/techsales-rfq/table/tech-sales-quotation-attachments-sheet.tsx')

diff --git a/lib/techsales-rfq/table/tech-sales-quotation-attachments-sheet.tsx b/lib/techsales-rfq/table/tech-sales-quotation-attachments-sheet.tsx
new file mode 100644
index 00000000..21c61773
--- /dev/null
+++ b/lib/techsales-rfq/table/tech-sales-quotation-attachments-sheet.tsx
@@ -0,0 +1,231 @@
+"use client"
+
+import * as React from "react"
+import {
+  Sheet,
+  SheetContent,
+  SheetHeader,
+  SheetTitle,
+  SheetDescription,
+} from "@/components/ui/sheet"
+import { Button } from "@/components/ui/button"
+import { Download, FileText, File, ImageIcon, AlertCircle } from "lucide-react"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import { formatDate } from "@/lib/utils"
+import prettyBytes from "pretty-bytes"
+
+// 견적서 첨부파일 타입 정의
+export interface QuotationAttachment {
+  id: number
+  quotationId: number
+  revisionId: number
+  fileName: string
+  originalFileName: string
+  fileSize: number
+  fileType: string | null
+  filePath: string
+  description: string | null
+  uploadedBy: number
+  vendorId: number
+  isVendorUpload: boolean
+  createdAt: Date
+  updatedAt: Date
+}
+
+// 견적서 정보 타입
+interface QuotationInfo {
+  id: number
+  quotationCode: string | null
+  vendorName?: string
+  rfqCode?: string
+}
+
+interface TechSalesQuotationAttachmentsSheetProps
+  extends React.ComponentPropsWithRef<typeof Sheet> {
+  quotation: QuotationInfo | null
+  attachments: QuotationAttachment[]
+  isLoading?: boolean
+}
+
+export function TechSalesQuotationAttachmentsSheet({
+  quotation,
+  attachments,
+  isLoading = false,
+  ...props
+}: TechSalesQuotationAttachmentsSheetProps) {
+  
+  // 파일 아이콘 선택 함수
+  const getFileIcon = (fileName: string) => {
+    const ext = fileName.split('.').pop()?.toLowerCase();
+    if (!ext) return <File className="h-5 w-5 text-gray-500" />;
+    
+    // 이미지 파일
+    if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(ext)) {
+      return <ImageIcon className="h-5 w-5 text-blue-500" />;
+    }
+    // PDF 파일
+    if (ext === 'pdf') {
+      return <FileText className="h-5 w-5 text-red-500" />;
+    }
+    // Excel 파일
+    if (['xlsx', 'xls', 'csv'].includes(ext)) {
+      return <FileText className="h-5 w-5 text-green-500" />;
+    }
+    // Word 파일
+    if (['docx', 'doc'].includes(ext)) {
+      return <FileText className="h-5 w-5 text-blue-500" />;
+    }
+    // 기본 파일
+    return <File className="h-5 w-5 text-gray-500" />;
+  };
+
+  // 파일 다운로드 처리
+  const handleDownload = (attachment: QuotationAttachment) => {
+    const link = document.createElement('a');
+    link.href = attachment.filePath;
+    link.download = attachment.originalFileName || attachment.fileName;
+    link.target = '_blank';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  };
+
+  // 리비전별로 첨부파일 그룹핑
+  const groupedAttachments = React.useMemo(() => {
+    const groups = new Map<number, QuotationAttachment[]>();
+    
+    attachments.forEach(attachment => {
+      const revisionId = attachment.revisionId;
+      if (!groups.has(revisionId)) {
+        groups.set(revisionId, []);
+      }
+      groups.get(revisionId)!.push(attachment);
+    });
+
+    // 리비전 ID 기준 내림차순 정렬 (최신 버전이 위에)
+    return Array.from(groups.entries())
+      .sort(([a], [b]) => b - a)
+      .map(([revisionId, files]) => ({
+        revisionId,
+        files: files.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+      }));
+  }, [attachments]);
+  return (
+    <Sheet {...props}>
+      <SheetContent className="flex flex-col gap-6 sm:max-w-md">
+        <SheetHeader className="text-left">
+          <SheetTitle>견적서 첨부파일</SheetTitle>
+          <SheetDescription>
+            <div className="space-y-1">
+              <div>견적서: {quotation?.quotationCode || "N/A"}</div>
+              {quotation?.vendorName && (
+                <div>벤더: {quotation.vendorName}</div>
+              )}
+              {quotation?.rfqCode && (
+                <div>RFQ: {quotation.rfqCode}</div>
+              )}
+            </div>
+          </SheetDescription>
+        </SheetHeader>
+
+        <div className="flex-1 overflow-auto">
+          {isLoading ? (
+            <div className="flex items-center justify-center py-8">
+              <div className="text-center">
+                <div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-2" />
+                <p className="text-sm text-muted-foreground">첨부파일 로딩 중...</p>
+              </div>
+            </div>
+          ) : attachments.length === 0 ? (
+            <div className="flex flex-col items-center justify-center py-8 text-center">
+              <AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
+              <p className="text-muted-foreground mb-2">첨부파일이 없습니다</p>
+              <p className="text-sm text-muted-foreground">
+                이 견적서에는 첨부된 파일이 없습니다.
+              </p>
+            </div>
+          ) : (
+            <div className="space-y-4">
+              <div className="flex items-center justify-between">
+                <h6 className="font-semibold text-sm">
+                  첨부파일 ({attachments.length}개)
+                </h6>
+              </div>
+              
+              {groupedAttachments.map((group, groupIndex) => (
+                <div key={group.revisionId} className="space-y-3">
+                  {/* 리비전 헤더 */}
+                  <div className="flex items-center gap-2">
+                    <Badge variant={group.revisionId === 0 ? "secondary" : "outline"} className="text-xs">
+                      {group.revisionId === 0 ? "초기 버전" : `버전 ${group.revisionId}`}
+                    </Badge>
+                    <span className="text-xs text-muted-foreground">
+                      ({group.files.length}개 파일)
+                    </span>
+                  </div>
+
+                  {/* 해당 리비전의 첨부파일들 */}
+                  {group.files.map((attachment) => (
+                    <div 
+                      key={attachment.id}
+                      className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors ml-4"
+                    >
+                      <div className="mt-1">
+                        {getFileIcon(attachment.fileName)}
+                      </div>
+                      
+                      <div className="flex-1 min-w-0">
+                        <div className="flex items-start justify-between gap-2">
+                          <div className="min-w-0 flex-1">
+                            <p className="text-sm font-medium break-words leading-tight">
+                              {attachment.originalFileName || attachment.fileName}
+                            </p>
+                            <div className="flex items-center gap-2 mt-1">
+                              <span className="text-xs text-muted-foreground">
+                                {prettyBytes(attachment.fileSize)}
+                              </span>
+                              <Badge variant="outline" className="text-xs">
+                                {attachment.isVendorUpload ? "벤더 업로드" : "시스템"}
+                              </Badge>
+                            </div>
+                            <p className="text-xs text-muted-foreground mt-1">
+                              {formatDate(attachment.createdAt)}
+                            </p>
+                            {attachment.description && (
+                              <p className="text-xs text-muted-foreground mt-1 break-words">
+                                {attachment.description}
+                              </p>
+                            )}
+                          </div>
+                        </div>
+                      </div>
+
+                      <div className="flex flex-col gap-1">
+                        {/* 다운로드 버튼 */}
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          className="h-8 w-8"
+                          onClick={() => handleDownload(attachment)}
+                          title="다운로드"
+                        >
+                          <Download className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </div>
+                  ))}
+
+                  {/* 그룹 간 구분선 (마지막 그룹 제외) */}
+                  {groupIndex < groupedAttachments.length - 1 && (
+                    <Separator className="my-4" />
+                  )}
+                </div>
+              ))}
+            </div>
+          )}
+        </div>
+      </SheetContent>
+    </Sheet>
+  )
+} 
\ No newline at end of file
-- 
cgit v1.2.3

