"use client" import * as React from "react" import { useState, useEffect, useRef } from "react" import { toast } from "sonner" import { Send, Paperclip, DownloadCloud, File, FileText, Image as ImageIcon, AlertCircle, X, User, Building } from "lucide-react" import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { formatDateTime, formatFileSize } from "@/lib/utils" import { useSession } from "next-auth/react" // 타입 정의 export interface TechSalesAttachment { id: number fileName: string originalFileName: string fileSize: number fileType: string | null filePath: string uploadedAt: Date } export interface TechSalesComment { id: number rfqId: number vendorId: number | null userId?: number | null content: string isVendorComment: boolean | null createdAt: Date updatedAt: Date userName?: string | null vendorName?: string | null attachments: TechSalesAttachment[] isRead: boolean | null } // 프롭스 정의 interface BuyerCommunicationDrawerProps { open: boolean; onOpenChange: (open: boolean) => void; quotation: { id: number; rfqId: number; vendorId: number; quotationCode: string | null; rfq?: { rfqCode: string | null; }; } | null; onSuccess?: () => void; } // 클라이언트에서 API를 통해 코멘트를 가져오는 함수 export async function fetchTechSalesVendorCommentsClient(rfqId: number, vendorId: number): Promise { const response = await fetch(`/api/tech-sales-rfqs/${rfqId}/vendors/${vendorId}/comments`); if (!response.ok) { throw new Error(`API 요청 실패: ${response.status}`); } const result = await response.json(); if (!result.success) { throw new Error(result.message || '코멘트 조회 중 오류가 발생했습니다'); } // API 응답 타입 정의 interface ApiComment { id: number; rfqId: number; vendorId: number | null; userId?: number | null; content: string; isVendorComment: boolean | null; createdAt: string; updatedAt: string; userName?: string | null; vendorName?: string | null; isRead: boolean | null; attachments: Array<{ id: number; fileName: string; originalFileName: string; fileSize: number; fileType: string | null; filePath: string; uploadedAt: string; }>; } return result.data.map((comment: ApiComment) => ({ ...comment, createdAt: new Date(comment.createdAt), updatedAt: new Date(comment.updatedAt), attachments: comment.attachments.map((att) => ({ ...att, uploadedAt: new Date(att.uploadedAt) })) })); } // 벤더 코멘트 전송 함수 export function sendVendorCommentClient(params: { rfqId: number; vendorId: number; content: string; attachments?: File[]; }): Promise { // 폼 데이터 생성 (파일 첨부를 위해) const formData = new FormData(); formData.append('rfqId', params.rfqId.toString()); formData.append('vendorId', params.vendorId.toString()); formData.append('content', params.content); formData.append('isVendorComment', 'true'); // 벤더가 보내는 메시지이므로 true // 첨부파일 추가 if (params.attachments && params.attachments.length > 0) { params.attachments.forEach((file) => { formData.append(`attachments`, file); }); } // API 엔드포인트 구성 (techsales API 경로) const url = `/api/tech-sales-rfqs/${params.rfqId}/vendors/${params.vendorId}/comments`; console.log("API 요청 시작:", { url, params }); // API 호출 return fetch(url, { method: 'POST', body: formData, // multipart/form-data 형식 사용 }) .then(response => { console.log("API 응답 상태:", response.status); if (!response.ok) { return response.text().then(text => { console.error("API 에러 응답:", text); throw new Error(`API 요청 실패: ${response.status} ${text}`); }); } return response.json(); }) .then(result => { console.log("API 응답 데이터:", result); if (!result.success || !result.data) { throw new Error(result.message || '코멘트 전송 중 오류가 발생했습니다'); } // API 응답 타입 정의 interface ApiAttachment { id: number; fileName: string; originalFileName: string; fileSize: number; fileType: string | null; filePath: string; uploadedAt: string; } interface ApiCommentResponse { id: number; rfqId: number; vendorId: number | null; userId?: number | null; content: string; isVendorComment: boolean | null; createdAt: string; updatedAt: string; userName?: string | null; isRead: boolean | null; attachments: ApiAttachment[]; } const commentData = result.data.comment as ApiCommentResponse; return { ...commentData, createdAt: new Date(commentData.createdAt), updatedAt: new Date(commentData.updatedAt), attachments: commentData.attachments.map((att) => ({ ...att, uploadedAt: new Date(att.uploadedAt) })) }; }) .catch(error => { console.error("클라이언트 API 호출 에러:", error); throw error; }); } export function BuyerCommunicationDrawer({ open, onOpenChange, quotation, onSuccess }: BuyerCommunicationDrawerProps) { // 세션 정보 const { data: session } = useSession(); // 상태 관리 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 [autoRefresh, setAutoRefresh] = useState(true); const [lastMessageCount, setLastMessageCount] = useState(0); const intervalRef = useRef(null); // 첨부파일 관련 상태 const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [selectedAttachment, setSelectedAttachment] = useState(null); // 드로어가 열릴 때 데이터 로드 useEffect(() => { if (open && quotation) { loadComments(); // 자동 새로고침 시작 if (autoRefresh) { startAutoRefresh(); } } else { // 드로어가 닫히면 자동 새로고침 중지 stopAutoRefresh(); } // 컴포넌트 언마운트 시 정리 return () => { stopAutoRefresh(); }; }, [open, quotation, autoRefresh]); // 스크롤 최하단으로 이동 useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [comments]); // 자동 새로고침 시작 const startAutoRefresh = () => { stopAutoRefresh(); // 기존 interval 정리 intervalRef.current = setInterval(() => { if (open && quotation && !isSubmitting) { loadComments(true); // 자동 새로고침임을 표시 } }, 60000); // 60초마다 새로고침 }; // 자동 새로고침 중지 const stopAutoRefresh = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; // 자동 새로고침 토글 const toggleAutoRefresh = () => { setAutoRefresh(prev => { const newValue = !prev; if (newValue && open) { startAutoRefresh(); } else { stopAutoRefresh(); } return newValue; }); }; // 코멘트 로드 함수 (자동 새로고침 여부 파라미터 추가) const loadComments = async (isAutoRefresh = false) => { if (!quotation) return; try { // 자동 새로고침일 때는 로딩 표시하지 않음 if (!isAutoRefresh) { setIsLoading(true); } // API를 사용하여 코멘트 데이터 가져오기 const commentsData = await fetchTechSalesVendorCommentsClient(quotation.rfqId, quotation.vendorId); // 새 메시지가 있는지 확인 (자동 새로고침일 때만) if (isAutoRefresh) { const newMessageCount = commentsData.length; if (newMessageCount > lastMessageCount && lastMessageCount > 0) { // 새 메시지 알림 toast.success(`새 메시지 ${newMessageCount - lastMessageCount}개가 도착했습니다`); } setLastMessageCount(newMessageCount); } else { setLastMessageCount(commentsData.length); } setComments(commentsData); // 읽음 상태 처리는 API 측에서 처리되는 것으로 가정 } catch (error) { console.error("코멘트 로드 오류:", error); if (!isAutoRefresh) { // 자동 새로고침일 때는 에러 토스트 표시하지 않음 toast.error("메시지를 불러오는 중 오류가 발생했습니다"); } } finally { // 항상 로딩 상태를 해제하되, 최소 200ms는 유지하여 깜빡거림 방지 if (!isAutoRefresh) { setTimeout(() => { setIsLoading(false); }, 200); } } }; // 파일 선택 핸들러 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)); }; // 코멘트 전송 핸들러 const handleSubmitComment = async () => { if (!newComment.trim() && attachments.length === 0) return; if (!quotation) return; try { setIsSubmitting(true); // API를 사용하여 새 코멘트 전송 (파일 업로드 때문에 FormData 사용) const newCommentObj = await sendVendorCommentClient({ rfqId: quotation.rfqId, vendorId: quotation.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: TechSalesAttachment) => { setSelectedAttachment(attachment); setPreviewDialogOpen(true); }; // 첨부파일 다운로드 핸들러 const handleDownloadClick = React.useCallback(async (attachment: TechSalesAttachment) => { try { const { downloadFile } = await import('@/lib/file-download'); await downloadFile(attachment.filePath, attachment.originalFileName || attachment.fileName, { showToast: true, onError: (error) => { console.error('다운로드 오류:', error); toast.error(error); }, onSuccess: (fileName, fileSize) => { console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`); } }); } catch (error) { console.error('다운로드 오류:', error); toast.error('파일 다운로드 중 오류가 발생했습니다.'); } }, []); // 파일 아이콘 선택 const getFileIcon = (fileType: string | null) => { if (!fileType) return ; 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/") || false; const isPdf = selectedAttachment.fileType?.includes("pdf") || false; return ( {getFileIcon(selectedAttachment.fileType)} {selectedAttachment.originalFileName || selectedAttachment.fileName} {formatFileSize(selectedAttachment.fileSize)} • {formatDateTime(selectedAttachment.uploadedAt)}
{isImage ? ( // eslint-disable-next-line @next/next/no-img-element {selectedAttachment.originalFileName ) : isPdf ? (