From e484964b1d78cedabbe182c789a8e4c9b53e29d3 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 29 May 2025 05:12:36 +0000 Subject: (김준회) 기술영업 조선 RFQ 파일첨부 및 채팅 기능 구현 / menuConfig 수정 (벤더 기술영업) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-response/detail/communication-tab.tsx | 292 ++++++++++----------- 1 file changed, 143 insertions(+), 149 deletions(-) (limited to 'lib/techsales-rfq/vendor-response/detail/communication-tab.tsx') diff --git a/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx b/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx index 0332232c..3f2a5280 100644 --- a/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx +++ b/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx @@ -1,21 +1,22 @@ "use client" import * as React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" -import { Separator } from "@/components/ui/separator" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { Send, MessageCircle } from "lucide-react" -import { formatDateTime } from "@/lib/utils" -import { toast } from "sonner" +import { Skeleton } from "@/components/ui/skeleton" +import { MessageSquare, Paperclip } from "lucide-react" +import { fetchTechSalesVendorCommentsClient, TechSalesComment } from "../buyer-communication-drawer" +import { BuyerCommunicationDrawer } from "../buyer-communication-drawer" interface CommunicationTabProps { quotation: { id: number + rfqId: number + vendorId: number + quotationCode: string | null rfq: { id: number rfqCode: string | null @@ -31,100 +32,73 @@ interface CommunicationTabProps { } } -// 임시 코멘트 데이터 (실제로는 API에서 가져와야 함) -const MOCK_COMMENTS = [ - { - id: 1, - content: "안녕하세요. 해당 자재에 대한 견적 요청 드립니다. 납기일은 언제까지 가능한지 문의드립니다.", - createdAt: new Date("2024-01-15T09:00:00"), - author: { - name: "김구매", - email: "buyer@company.com", - role: "구매담당자" - } - }, - { - id: 2, - content: "안녕하세요. 견적 요청 확인했습니다. 해당 자재의 경우 약 2주 정도의 제작 기간이 필요합니다. 상세한 견적은 내일까지 제출하겠습니다.", - createdAt: new Date("2024-01-15T14:30:00"), - author: { - name: "이벤더", - email: "vendor@supplier.com", - role: "벤더" - } - }, - { - id: 3, - content: "감사합니다. 추가로 품질 인증서도 함께 제출 가능한지 확인 부탁드립니다.", - createdAt: new Date("2024-01-16T10:15:00"), - author: { - name: "김구매", - email: "buyer@company.com", - role: "구매담당자" - } - } -] - export function CommunicationTab({ quotation }: CommunicationTabProps) { - const [newComment, setNewComment] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [comments, setComments] = useState(MOCK_COMMENTS) + const [comments, setComments] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [loadingComments, setLoadingComments] = useState(false); + const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false); - const handleSendComment = async () => { - if (!newComment.trim()) { - toast.error("메시지를 입력해주세요.") - return + // 컴포넌트 마운트 시 메시지 미리 로드 + useEffect(() => { + if (quotation) { + loadCommunicationData(); } + }, [quotation]); - setIsLoading(true) + // 메시지 데이터 로드 함수 + const loadCommunicationData = async () => { try { - // TODO: API 호출로 코멘트 전송 - const newCommentData = { - id: comments.length + 1, - content: newComment, - createdAt: new Date(), - author: { - name: "현재사용자", // 실제로는 세션에서 가져와야 함 - email: "current@user.com", - role: "벤더" - } - } - - setComments([...comments, newCommentData]) - setNewComment("") - toast.success("메시지가 전송되었습니다.") - } catch { - toast.error("메시지 전송 중 오류가 발생했습니다.") + setLoadingComments(true); + const commentsData = await fetchTechSalesVendorCommentsClient(quotation.rfqId, quotation.vendorId); + setComments(commentsData); + + // 읽지 않은 메시지 수 계산 (구매자가 보낸 메시지 중 읽지 않은 것) + const unread = commentsData.filter( + comment => !comment.isVendorComment && !comment.isRead + ).length; + setUnreadCount(unread); + } catch (error) { + console.error("메시지 데이터 로드 오류:", error); } finally { - setIsLoading(false) + setLoadingComments(false); } - } + }; - const getAuthorInitials = (name: string) => { - return name - .split(" ") - .map(word => word[0]) - .join("") - .toUpperCase() - .slice(0, 2) - } - - const getRoleBadgeVariant = (role: string) => { - return role === "구매담당자" ? "default" : "secondary" - } + // 커뮤니케이션 드로어가 닫힐 때 데이터 새로고침 + const handleCommunicationDrawerChange = (open: boolean) => { + setCommunicationDrawerOpen(open); + if (!open) { + loadCommunicationData(); // 드로어가 닫힐 때 데이터 새로고침 + } + }; return (
{/* 헤더 */} - - - - 커뮤니케이션 - - - RFQ {quotation.rfq?.rfqCode || "미할당"}에 대한 구매담당자와의 커뮤니케이션 - + +
+ + + 커뮤니케이션 + {unreadCount > 0 && ( + + 새 메시지 {unreadCount} + + )} + + + RFQ {quotation.rfq?.rfqCode || "미할당"}에 대한 구매담당자와의 커뮤니케이션 + +
+
@@ -135,81 +109,101 @@ export function CommunicationTab({ quotation }: CommunicationTabProps) { - {/* 메시지 목록 */} + {/* 메시지 미리보기 */} 메시지 ({comments.length}) - - -
- {comments.length === 0 ? ( -
- -

아직 메시지가 없습니다.

-

첫 번째 메시지를 보내보세요.

+ + {loadingComments ? ( +
+
+ + +
+
+ ) : comments.length === 0 ? ( +
+
+
+
- ) : ( - comments.map((comment) => ( -
- - - {getAuthorInitials(comment.author.name)} - - -
-
- {comment.author.name} - - {comment.author.role} - +

아직 메시지가 없습니다

+

+ 견적서에 대한 질문이나 의견이 있으신가요? 구매자와 메시지를 주고받으세요. +

+ +
+
+ ) : ( +
+ {/* 최근 메시지 3개 미리보기 */} +
+

최근 메시지

+ + {comments.slice(-3).map(comment => ( +
+
+ + {comment.isVendorComment + ? '나' + : comment.userName || '구매 담당자'} + - {formatDateTime(comment.createdAt)} + {new Date(comment.createdAt).toLocaleDateString()}
-
- {comment.content} -
+

{comment.content}

+ {comment.attachments.length > 0 && ( +
+ + 첨부파일 {comment.attachments.length}개 +
+ )}
-
- )) - )} -
- - - + ))} + +
- {/* 새 메시지 입력 */} -
-