diff options
| author | joonhoekim <26rote@gmail.com> | 2025-05-29 05:12:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-29 05:37:04 +0000 |
| commit | e484964b1d78cedabbe182c789a8e4c9b53e29d3 (patch) | |
| tree | d18133dde99e6feb773c95d04f7e79715ab24252 /lib/techsales-rfq/vendor-response/detail/communication-tab.tsx | |
| parent | 37f55540833c2d5894513eca9fc8f7c6233fc2d2 (diff) | |
(김준회) 기술영업 조선 RFQ 파일첨부 및 채팅 기능 구현 / menuConfig 수정 (벤더 기술영업)
Diffstat (limited to 'lib/techsales-rfq/vendor-response/detail/communication-tab.tsx')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/detail/communication-tab.tsx | 292 |
1 files changed, 143 insertions, 149 deletions
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<TechSalesComment[]>([]); + 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 ( <div className="h-full flex flex-col"> {/* 헤더 */} <Card className="mb-4"> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <MessageCircle className="h-5 w-5" /> - 커뮤니케이션 - </CardTitle> - <CardDescription> - RFQ {quotation.rfq?.rfqCode || "미할당"}에 대한 구매담당자와의 커뮤니케이션 - </CardDescription> + <CardHeader className="flex flex-row items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + <MessageSquare className="h-5 w-5" /> + 커뮤니케이션 + {unreadCount > 0 && ( + <Badge variant="destructive" className="ml-2"> + 새 메시지 {unreadCount} + </Badge> + )} + </CardTitle> + <CardDescription> + RFQ {quotation.rfq?.rfqCode || "미할당"}에 대한 구매담당자와의 커뮤니케이션 + </CardDescription> + </div> + <Button + onClick={() => setCommunicationDrawerOpen(true)} + variant="outline" + size="sm" + > + <MessageSquare className="h-4 w-4 mr-2" /> + {unreadCount > 0 ? "새 메시지 확인" : "메시지 보내기"} + </Button> </CardHeader> <CardContent> <div className="flex items-center gap-4 text-sm text-muted-foreground"> @@ -135,81 +109,101 @@ export function CommunicationTab({ quotation }: CommunicationTabProps) { </CardContent> </Card> - {/* 메시지 목록 */} + {/* 메시지 미리보기 */} <Card className="flex-1 flex flex-col min-h-0"> <CardHeader> <CardTitle className="text-lg">메시지 ({comments.length})</CardTitle> </CardHeader> - <CardContent className="flex-1 flex flex-col min-h-0"> - <ScrollArea className="flex-1 pr-4"> - <div className="space-y-4"> - {comments.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - <MessageCircle className="h-12 w-12 mx-auto mb-4 opacity-50" /> - <p>아직 메시지가 없습니다.</p> - <p className="text-sm">첫 번째 메시지를 보내보세요.</p> + <CardContent> + {loadingComments ? ( + <div className="flex items-center justify-center p-8"> + <div className="text-center"> + <Skeleton className="h-4 w-32 mx-auto mb-2" /> + <Skeleton className="h-4 w-48 mx-auto" /> + </div> + </div> + ) : comments.length === 0 ? ( + <div className="min-h-[200px] flex flex-col items-center justify-center text-center p-8"> + <div className="max-w-md"> + <div className="mx-auto bg-primary/10 rounded-full w-12 h-12 flex items-center justify-center mb-4"> + <MessageSquare className="h-6 w-6 text-primary" /> </div> - ) : ( - comments.map((comment) => ( - <div key={comment.id} className="flex gap-3"> - <Avatar className="h-8 w-8 mt-1"> - <AvatarFallback className="text-xs"> - {getAuthorInitials(comment.author.name)} - </AvatarFallback> - </Avatar> - <div className="flex-1 space-y-2"> - <div className="flex items-center gap-2"> - <span className="font-medium text-sm">{comment.author.name}</span> - <Badge variant={getRoleBadgeVariant(comment.author.role)} className="text-xs"> - {comment.author.role} - </Badge> + <h3 className="text-lg font-medium mb-2">아직 메시지가 없습니다</h3> + <p className="text-muted-foreground mb-4"> + 견적서에 대한 질문이나 의견이 있으신가요? 구매자와 메시지를 주고받으세요. + </p> + <Button + onClick={() => setCommunicationDrawerOpen(true)} + className="mx-auto" + > + 메시지 보내기 + </Button> + </div> + </div> + ) : ( + <div className="space-y-4"> + {/* 최근 메시지 3개 미리보기 */} + <div className="space-y-2"> + <h3 className="text-sm font-medium">최근 메시지</h3> + <ScrollArea className="h-[250px] rounded-md border p-4"> + {comments.slice(-3).map(comment => ( + <div + key={comment.id} + className={`p-3 mb-3 rounded-lg ${!comment.isVendorComment && !comment.isRead + ? 'bg-primary/10 border-l-4 border-primary' + : 'bg-muted/50' + }`} + > + <div className="flex justify-between items-center mb-1"> + <span className="text-sm font-medium"> + {comment.isVendorComment + ? '나' + : comment.userName || '구매 담당자'} + </span> <span className="text-xs text-muted-foreground"> - {formatDateTime(comment.createdAt)} + {new Date(comment.createdAt).toLocaleDateString()} </span> </div> - <div className="bg-muted p-3 rounded-lg text-sm"> - {comment.content} - </div> + <p className="text-sm line-clamp-2">{comment.content}</p> + {comment.attachments.length > 0 && ( + <div className="mt-1 text-xs text-muted-foreground"> + <Paperclip className="h-3 w-3 inline mr-1" /> + 첨부파일 {comment.attachments.length}개 + </div> + )} </div> - </div> - )) - )} - </div> - </ScrollArea> - - <Separator className="my-4" /> + ))} + </ScrollArea> + </div> - {/* 새 메시지 입력 */} - <div className="space-y-3"> - <Textarea - placeholder="메시지를 입력하세요..." - value={newComment} - onChange={(e) => setNewComment(e.target.value)} - rows={3} - className="resize-none" - onKeyDown={(e) => { - if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - handleSendComment() - } - }} - /> - <div className="flex justify-between items-center"> - <div className="text-xs text-muted-foreground"> - Ctrl + Enter로 빠른 전송 + <div className="flex justify-center"> + <Button + onClick={() => setCommunicationDrawerOpen(true)} + className="w-full" + > + 전체 메시지 보기 ({comments.length}개) + </Button> </div> - <Button - onClick={handleSendComment} - disabled={isLoading || !newComment.trim()} - size="sm" - > - <Send className="h-4 w-4 mr-2" /> - 전송 - </Button> </div> - </div> + )} </CardContent> </Card> + + {/* 커뮤니케이션 드로어 */} + <BuyerCommunicationDrawer + open={communicationDrawerOpen} + onOpenChange={handleCommunicationDrawerChange} + quotation={{ + id: quotation.id, + rfqId: quotation.rfqId, + vendorId: quotation.vendorId, + quotationCode: quotation.quotationCode, + rfq: quotation.rfq ? { + rfqCode: quotation.rfq.rfqCode + } : undefined + }} + onSuccess={loadCommunicationData} + /> </div> ) }
\ No newline at end of file |
