summaryrefslogtreecommitdiff
path: root/lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx')
-rw-r--r--lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx522
1 files changed, 0 insertions, 522 deletions
diff --git a/lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx b/lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx
deleted file mode 100644
index 69ba0363..00000000
--- a/lib/procurement-rfqs/vendor-response/buyer-communication-drawer.tsx
+++ /dev/null
@@ -1,522 +0,0 @@
-"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 { ScrollArea } from "@/components/ui/scroll-area"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { formatDateTime, formatFileSize } from "@/lib/utils"
-import { useSession } from "next-auth/react"
-import { fetchBuyerVendorComments } from "../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 | null; // null 허용으로 변경
- filePath: string;
- uploadedAt: Date;
-}
-
-// 프롭스 정의
-interface BuyerCommunicationDrawerProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- quotation: {
- id: number;
- rfqId: number;
- vendorId: number;
- quotationCode: string;
- rfq?: {
- rfqCode: string;
- };
- } | null;
- onSuccess?: () => void;
-}
-
-
-
-// 벤더 코멘트 전송 함수
-export function sendVendorCommentClient(params: {
- rfqId: number;
- vendorId: number;
- content: string;
- attachments?: File[];
-}): Promise<Comment> {
- // 폼 데이터 생성 (파일 첨부를 위해)
- 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 엔드포인트 구성 (벤더 API 경로)
- const url = `/api/procurement-rfqs/${params.rfqId}/vendors/${params.vendorId}/comments`;
-
- // API 호출
- return fetch(url, {
- method: 'POST',
- body: formData, // multipart/form-data 형식 사용
- })
- .then(response => {
- if (!response.ok) {
- return response.text().then(text => {
- throw new Error(`API 요청 실패: ${response.status} ${text}`);
- });
- }
- return response.json();
- })
- .then(result => {
- if (!result.success || !result.data) {
- throw new Error(result.message || '코멘트 전송 중 오류가 발생했습니다');
- }
- return result.data.comment;
- });
-}
-
-
-export function BuyerCommunicationDrawer({
- open,
- onOpenChange,
- quotation,
- onSuccess
-}: BuyerCommunicationDrawerProps) {
- // 세션 정보
- const { data: session } = useSession();
-
- // 상태 관리
- const [comments, setComments] = useState<Comment[]>([]);
- const [newComment, setNewComment] = useState("");
- const [attachments, setAttachments] = useState<File[]>([]);
- const [isLoading, setIsLoading] = useState(false);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const fileInputRef = useRef<HTMLInputElement>(null);
- const messagesEndRef = useRef<HTMLDivElement>(null);
-
- // 첨부파일 관련 상태
- const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
- const [selectedAttachment, setSelectedAttachment] = useState<Attachment | null>(null);
-
- // 드로어가 열릴 때 데이터 로드
- useEffect(() => {
- if (open && quotation) {
- loadComments();
- }
- }, [open, quotation]);
-
- // 스크롤 최하단으로 이동
- useEffect(() => {
- if (messagesEndRef.current) {
- messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
- }
- }, [comments]);
-
- // 코멘트 로드 함수
- const loadComments = async () => {
- if (!quotation) return;
-
- try {
- setIsLoading(true);
-
- // API를 사용하여 코멘트 데이터 가져오기
- const commentsData = await fetchBuyerVendorComments(quotation.rfqId, quotation.vendorId);
- setComments(commentsData);
-
- // 읽음 상태 처리는 API 측에서 처리되는 것으로 가정
- } catch (error) {
- console.error("코멘트 로드 오류:", error);
- toast.error("메시지를 불러오는 중 오류가 발생했습니다");
- } finally {
- setIsLoading(false);
- }
- };
-
- // 파일 선택 핸들러
- const handleFileSelect = () => {
- fileInputRef.current?.click();
- };
-
- // 파일 변경 핸들러
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- 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: Attachment) => {
- setSelectedAttachment(attachment);
- setPreviewDialogOpen(true);
- };
-
- // 첨부파일 다운로드
- const handleAttachmentDownload = (attachment: Attachment) => {
- // 실제 다운로드 구현
- window.open(attachment.filePath, '_blank');
- };
-
- // 파일 아이콘 선택
- const getFileIcon = (fileType: string) => {
- if (fileType.startsWith("image/")) return <ImageIcon className="h-5 w-5 text-blue-500" />;
- if (fileType.includes("pdf")) return <FileText className="h-5 w-5 text-red-500" />;
- if (fileType.includes("spreadsheet") || fileType.includes("excel"))
- return <FileText className="h-5 w-5 text-green-500" />;
- if (fileType.includes("document") || fileType.includes("word"))
- return <FileText className="h-5 w-5 text-blue-500" />;
- return <File className="h-5 w-5 text-gray-500" />;
- };
-
- // 첨부파일 미리보기 다이얼로그
- const renderAttachmentPreviewDialog = () => {
- if (!selectedAttachment) return null;
-
- const isImage = selectedAttachment.fileType.startsWith("image/");
- const isPdf = selectedAttachment.fileType.includes("pdf");
-
- return (
- <Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
- <DialogContent className="max-w-3xl">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- {getFileIcon(selectedAttachment.fileType)}
- {selectedAttachment.fileName}
- </DialogTitle>
- <DialogDescription>
- {formatFileSize(selectedAttachment.fileSize)} • {formatDateTime(selectedAttachment.uploadedAt)}
- </DialogDescription>
- </DialogHeader>
-
- <div className="min-h-[300px] flex items-center justify-center p-4">
- {isImage ? (
- <img
- src={selectedAttachment.filePath}
- alt={selectedAttachment.fileName}
- className="max-h-[500px] max-w-full object-contain"
- />
- ) : isPdf ? (
- <iframe
- src={`${selectedAttachment.filePath}#toolbar=0`}
- className="w-full h-[500px]"
- title={selectedAttachment.fileName}
- />
- ) : (
- <div className="flex flex-col items-center gap-4 p-8">
- {getFileIcon(selectedAttachment.fileType)}
- <p className="text-muted-foreground text-sm">미리보기를 지원하지 않는 파일 형식입니다.</p>
- <Button
- variant="outline"
- onClick={() => handleAttachmentDownload(selectedAttachment)}
- >
- <DownloadCloud className="h-4 w-4 mr-2" />
- 다운로드
- </Button>
- </div>
- )}
- </div>
- </DialogContent>
- </Dialog>
- );
- };
-
- if (!quotation) {
- return null;
- }
-
- // 구매자 정보 (실제로는 API에서 가져와야 함)
- const buyerName = "구매 담당자";
-
- return (
- <Drawer open={open} onOpenChange={onOpenChange}>
- <DrawerContent className="max-h-[85vh]">
- <DrawerHeader className="border-b">
- <DrawerTitle className="flex items-center gap-2">
- <Avatar className="h-8 w-8">
- <AvatarFallback className="bg-primary/10">
- <User className="h-4 w-4" />
- </AvatarFallback>
- </Avatar>
- <div>
- <span>{buyerName}</span>
- <Badge variant="outline" className="ml-2">구매자</Badge>
- </div>
- </DrawerTitle>
- <DrawerDescription>
- RFQ: {quotation.rfq?.rfqCode || "N/A"} • 견적서: {quotation.quotationCode}
- </DrawerDescription>
- </DrawerHeader>
-
- <div className="p-0 flex flex-col h-[60vh]">
- {/* 메시지 목록 */}
- <ScrollArea className="flex-1 p-4">
- {isLoading ? (
- <div className="flex h-full items-center justify-center">
- <p className="text-muted-foreground">메시지 로딩 중...</p>
- </div>
- ) : comments.length === 0 ? (
- <div className="flex h-full items-center justify-center">
- <div className="flex flex-col items-center gap-2">
- <AlertCircle className="h-6 w-6 text-muted-foreground" />
- <p className="text-muted-foreground">아직 메시지가 없습니다</p>
- </div>
- </div>
- ) : (
- <div className="space-y-4">
- {comments.map(comment => (
- <div
- key={comment.id}
- className={`flex gap-3 ${comment.isVendorComment ? 'justify-end' : 'justify-start'}`}
- >
- {!comment.isVendorComment && (
- <Avatar className="h-8 w-8 mt-1">
- <AvatarFallback className="bg-primary/10">
- <User className="h-4 w-4" />
- </AvatarFallback>
- </Avatar>
- )}
-
- <div className={`rounded-lg p-3 max-w-[80%] ${comment.isVendorComment
- ? 'bg-primary text-primary-foreground'
- : 'bg-muted'
- }`}>
- <div className="text-sm font-medium mb-1">
- {comment.isVendorComment ? (
- session?.user?.name || "벤더"
- ) : (
- comment.userName || buyerName
- )}
- </div>
-
- {comment.content && (
- <div className="text-sm whitespace-pre-wrap break-words">
- {comment.content}
- </div>
- )}
-
- {/* 첨부파일 표시 */}
- {comment.attachments.length > 0 && (
- <div className={`mt-2 pt-2 ${comment.isVendorComment
- ? 'border-t border-t-primary-foreground/20'
- : 'border-t border-t-border/30'
- }`}>
- {comment.attachments.map(attachment => (
- <div
- key={attachment.id}
- className="flex items-center text-xs gap-2 mb-1 p-1 rounded hover:bg-black/5 cursor-pointer"
- onClick={() => handleAttachmentPreview(attachment)}
- >
- {getFileIcon(attachment.fileType)}
- <span className="flex-1 truncate">{attachment.fileName}</span>
- <span className="text-xs opacity-70">
- {formatFileSize(attachment.fileSize)}
- </span>
- <Button
- variant="ghost"
- size="icon"
- className="h-6 w-6 rounded-full"
- onClick={(e) => {
- e.stopPropagation();
- handleAttachmentDownload(attachment);
- }}
- >
- <DownloadCloud className="h-3 w-3" />
- </Button>
- </div>
- ))}
- </div>
- )}
-
- <div className="text-xs mt-1 opacity-70 flex items-center gap-1 justify-end">
- {formatDateTime(comment.createdAt)}
- </div>
- </div>
-
- {comment.isVendorComment && (
- <Avatar className="h-8 w-8 mt-1">
- <AvatarFallback className="bg-primary/20">
- <Building className="h-4 w-4" />
- </AvatarFallback>
- </Avatar>
- )}
- </div>
- ))}
- <div ref={messagesEndRef} />
- </div>
- )}
- </ScrollArea>
-
- {/* 선택된 첨부파일 표시 */}
- {attachments.length > 0 && (
- <div className="p-2 bg-muted mx-4 rounded-md mb-2">
- <div className="text-xs font-medium mb-1">첨부파일</div>
- <div className="flex flex-wrap gap-2">
- {attachments.map((file, index) => (
- <div key={index} className="flex items-center bg-background rounded-md p-1 pr-2 text-xs">
- {file.type.startsWith("image/") ? (
- <ImageIcon className="h-4 w-4 mr-1 text-blue-500" />
- ) : (
- <File className="h-4 w-4 mr-1 text-gray-500" />
- )}
- <span className="truncate max-w-[100px]">{file.name}</span>
- <Button
- variant="ghost"
- size="icon"
- className="h-4 w-4 ml-1 p-0"
- onClick={() => handleRemoveFile(index)}
- >
- <X className="h-3 w-3" />
- </Button>
- </div>
- ))}
- </div>
- </div>
- )}
-
- {/* 메시지 입력 영역 */}
- <div className="p-4 border-t">
- <div className="flex gap-2 items-end">
- <div className="flex-1">
- <Textarea
- placeholder="메시지를 입력하세요..."
- className="min-h-[80px] resize-none"
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
- />
- </div>
- <div className="flex flex-col gap-2">
- <input
- type="file"
- ref={fileInputRef}
- className="hidden"
- multiple
- onChange={handleFileChange}
- />
- <Button
- variant="outline"
- size="icon"
- onClick={handleFileSelect}
- title="파일 첨부"
- >
- <Paperclip className="h-4 w-4" />
- </Button>
- <Button
- onClick={handleSubmitComment}
- disabled={(!newComment.trim() && attachments.length === 0) || isSubmitting}
- >
- <Send className="h-4 w-4" />
- </Button>
- </div>
- </div>
- </div>
- </div>
-
- <DrawerFooter className="border-t">
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => loadComments()}>
- 새로고침
- </Button>
- <DrawerClose asChild>
- <Button variant="outline">닫기</Button>
- </DrawerClose>
- </div>
- </DrawerFooter>
- </DrawerContent>
-
- {renderAttachmentPreviewDialog()}
- </Drawer>
- );
-} \ No newline at end of file