From 9eb8e80f4f736c4edffa650c685d1f170ca51aa1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 15 May 2025 01:19:49 +0000 Subject: (대표님) 구매 요청사항 반영한 통합 rfq / 필터 개인화 / po-rfq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail-table/vendor-communication-drawer.tsx | 518 +++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 components/po-rfq/detail-table/vendor-communication-drawer.tsx (limited to 'components/po-rfq/detail-table/vendor-communication-drawer.tsx') diff --git a/components/po-rfq/detail-table/vendor-communication-drawer.tsx b/components/po-rfq/detail-table/vendor-communication-drawer.tsx new file mode 100644 index 00000000..34efdfc2 --- /dev/null +++ b/components/po-rfq/detail-table/vendor-communication-drawer.tsx @@ -0,0 +1,518 @@ +"use client" + +import * as React from "react" +import { useState, useEffect, useRef } from "react" +import { ProcurementRfqsView } from "@/db/schema" +import { RfqDetailView } from "./rfq-detail-column" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, +} from "@/components/ui/drawer" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Badge } from "@/components/ui/badge" +import { toast } from "sonner" +import { + Send, + Paperclip, + DownloadCloud, + File, + FileText, + Image as ImageIcon, + AlertCircle, + X +} from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { formatDateTime } from "@/lib/utils" +import { formatFileSize } from "@/lib/utils" // formatFileSize 유틸리티 임포트 +import { fetchVendorComments, markMessagesAsRead } from "@/lib/procurement-rfqs/services" + +// 타입 정의 +interface Comment { + id: number; + rfqId: number; + vendorId: number | null // null 허용으로 변경 + userId?: number | null // null 허용으로 변경 + content: string; + isVendorComment: boolean | null; // null 허용으로 변경 + createdAt: Date; + updatedAt: Date; + userName?: string | null // null 허용으로 변경 + vendorName?: string | null // null 허용으로 변경 + attachments: Attachment[]; + isRead: boolean | null // null 허용으로 변경 +} + +interface Attachment { + id: number; + fileName: string; + fileSize: number; + fileType: string; + filePath: string; + uploadedAt: Date; +} + +// 프롭스 정의 +interface VendorCommunicationDrawerProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedRfq: ProcurementRfqsView | null; + selectedVendor: RfqDetailView | null; + onSuccess?: () => void; +} + +async function sendComment(params: { + rfqId: number; + vendorId: number; + content: string; + attachments?: File[]; +}): Promise { + try { + // 폼 데이터 생성 (파일 첨부를 위해) + const formData = new FormData(); + formData.append('rfqId', params.rfqId.toString()); + formData.append('vendorId', params.vendorId.toString()); + formData.append('content', params.content); + formData.append('isVendorComment', 'false'); + + // 첨부파일 추가 + if (params.attachments && params.attachments.length > 0) { + params.attachments.forEach((file) => { + formData.append(`attachments`, file); + }); + } + + // API 엔드포인트 구성 + const url = `/api/procurement-rfqs/${params.rfqId}/vendors/${params.vendorId}/comments`; + + // API 호출 + const response = await fetch(url, { + method: 'POST', + body: formData, // multipart/form-data 형식 사용 + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API 요청 실패: ${response.status} ${errorText}`); + } + + // 응답 데이터 파싱 + const result = await response.json(); + + if (!result.success || !result.data) { + throw new Error(result.message || '코멘트 전송 중 오류가 발생했습니다'); + } + + return result.data.comment; + } catch (error) { + console.error('코멘트 전송 오류:', error); + throw error; + } +} + +export function VendorCommunicationDrawer({ + open, + onOpenChange, + selectedRfq, + selectedVendor, + onSuccess +}: VendorCommunicationDrawerProps) { + // 상태 관리 + const [comments, setComments] = useState([]); + const [newComment, setNewComment] = useState(""); + const [attachments, setAttachments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const fileInputRef = useRef(null); + const messagesEndRef = useRef(null); + + // 첨부파일 관련 상태 + const [previewDialogOpen, setPreviewDialogOpen] = useState(false); + const [selectedAttachment, setSelectedAttachment] = useState(null); + + // 드로어가 열릴 때 데이터 로드 + useEffect(() => { + if (open && selectedRfq && selectedVendor) { + loadComments(); + } + }, [open, selectedRfq, selectedVendor]); + + // 스크롤 최하단으로 이동 + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [comments]); + + // 코멘트 로드 함수 + const loadComments = async () => { + if (!selectedRfq || !selectedVendor) return; + + try { + setIsLoading(true); + + // Server Action을 사용하여 코멘트 데이터 가져오기 + const commentsData = await fetchVendorComments(selectedRfq.id, selectedVendor.vendorId); + setComments(commentsData); + + // Server Action을 사용하여 읽지 않은 메시지를 읽음 상태로 변경 + await markMessagesAsRead(selectedRfq.id, selectedVendor.vendorId); + } catch (error) { + console.error("코멘트 로드 오류:", error); + toast.error("메시지를 불러오는 중 오류가 발생했습니다"); + } finally { + setIsLoading(false); + } + }; + + // 파일 선택 핸들러 + const handleFileSelect = () => { + fileInputRef.current?.click(); + }; + + // 파일 변경 핸들러 + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const newFiles = Array.from(e.target.files); + setAttachments(prev => [...prev, ...newFiles]); + } + }; + + // 파일 제거 핸들러 + const handleRemoveFile = (index: number) => { + setAttachments(prev => prev.filter((_, i) => i !== index)); + }; + + console.log(newComment) + + // 코멘트 전송 핸들러 + const handleSubmitComment = async () => { + console.log("버튼 클릭1", selectedRfq,selectedVendor, selectedVendor?.vendorId ) + console.log(!newComment.trim() && attachments.length === 0) + + if (!newComment.trim() && attachments.length === 0) return; + if (!selectedRfq || !selectedVendor || !selectedVendor.vendorId) return; + + console.log("버튼 클릭") + + try { + setIsSubmitting(true); + + // API를 사용하여 새 코멘트 전송 (파일 업로드 때문에 FormData 사용) + const newCommentObj = await sendComment({ + rfqId: selectedRfq.id, + vendorId: selectedVendor.vendorId, + content: newComment, + attachments: attachments + }); + + // 상태 업데이트 + setComments(prev => [...prev, newCommentObj]); + setNewComment(""); + setAttachments([]); + + toast.success("메시지가 전송되었습니다"); + + // 데이터 새로고침 + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error("코멘트 전송 오류:", error); + toast.error("메시지 전송 중 오류가 발생했습니다"); + } finally { + setIsSubmitting(false); + } + }; + + // 첨부파일 미리보기 + const handleAttachmentPreview = (attachment: Attachment) => { + setSelectedAttachment(attachment); + setPreviewDialogOpen(true); + }; + + // 첨부파일 다운로드 + const handleAttachmentDownload = (attachment: Attachment) => { + // TODO: 실제 다운로드 구현 + window.open(attachment.filePath, '_blank'); + }; + + // 파일 아이콘 선택 + const getFileIcon = (fileType: string) => { + if (fileType.startsWith("image/")) return ; + if (fileType.includes("pdf")) return ; + if (fileType.includes("spreadsheet") || fileType.includes("excel")) + return ; + if (fileType.includes("document") || fileType.includes("word")) + return ; + return ; + }; + + // 첨부파일 미리보기 다이얼로그 + const renderAttachmentPreviewDialog = () => { + if (!selectedAttachment) return null; + + const isImage = selectedAttachment.fileType.startsWith("image/"); + const isPdf = selectedAttachment.fileType.includes("pdf"); + + return ( + + + + + {getFileIcon(selectedAttachment.fileType)} + {selectedAttachment.fileName} + + + {formatFileSize(selectedAttachment.fileSize)} • {formatDateTime(selectedAttachment.uploadedAt)} + + + +
+ {isImage ? ( + {selectedAttachment.fileName} + ) : isPdf ? ( +