diff options
Diffstat (limited to 'lib/techsales-rfq/table/detail-table')
| -rw-r--r-- | lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx | 140 |
1 files changed, 119 insertions, 21 deletions
diff --git a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx index 958cc8d1..4172ccd7 100644 --- a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx +++ b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx @@ -15,7 +15,6 @@ import { DrawerHeader, DrawerTitle, } from "@/components/ui/drawer" -import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" import { toast } from "sonner" import { @@ -143,6 +142,11 @@ export function VendorCommunicationDrawer({ const fileInputRef = useRef<HTMLInputElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null); + // 자동 새로고침 관련 상태 + const [autoRefresh, setAutoRefresh] = useState(true); + const [lastMessageCount, setLastMessageCount] = useState(0); + const intervalRef = useRef<NodeJS.Timeout | null>(null); + // 첨부파일 관련 상태 const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [selectedAttachment, setSelectedAttachment] = useState<Attachment | null>(null); @@ -151,8 +155,20 @@ export function VendorCommunicationDrawer({ useEffect(() => { if (open && selectedRfq && selectedVendor) { loadComments(); + // 자동 새로고침 시작 + if (autoRefresh) { + startAutoRefresh(); + } + } else { + // 드로어가 닫히면 자동 새로고침 중지 + stopAutoRefresh(); } - }, [open, selectedRfq, selectedVendor]); + + // 컴포넌트 언마운트 시 정리 + return () => { + stopAutoRefresh(); + }; + }, [open, selectedRfq, selectedVendor, autoRefresh]); // 스크롤 최하단으로 이동 useEffect(() => { @@ -160,25 +176,79 @@ export function VendorCommunicationDrawer({ messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [comments]); + + // 자동 새로고침 시작 + const startAutoRefresh = () => { + stopAutoRefresh(); // 기존 interval 정리 + intervalRef.current = setInterval(() => { + if (open && selectedRfq && selectedVendor && !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 () => { + // 코멘트 로드 함수 (자동 새로고침 여부 파라미터 추가) + const loadComments = async (isAutoRefresh = false) => { if (!selectedRfq || !selectedVendor) return; try { - setIsLoading(true); + // 자동 새로고침일 때는 로딩 표시하지 않음 + if (!isAutoRefresh) { + setIsLoading(true); + } // Server Action을 사용하여 코멘트 데이터 가져오기 const commentsData = await fetchTechSalesVendorComments(selectedRfq.id, selectedVendor.vendorId || 0); + + // 새 메시지가 있는지 확인 (자동 새로고침일 때만) + if (isAutoRefresh) { + const newMessageCount = commentsData.length; + if (newMessageCount > lastMessageCount && lastMessageCount > 0) { + // 새 메시지 알림 (선택사항) + toast.success(`새 메시지 ${newMessageCount - lastMessageCount}개가 도착했습니다`); + } + setLastMessageCount(newMessageCount); + } else { + setLastMessageCount(commentsData.length); + } + setComments(commentsData as Comment[]); // 구체적인 타입으로 캐스팅 // Server Action을 사용하여 읽지 않은 메시지를 읽음 상태로 변경 await markTechSalesMessagesAsRead(selectedRfq.id, selectedVendor.vendorId || 0); } catch (error) { console.error("코멘트 로드 오류:", error); - toast.error("메시지를 불러오는 중 오류가 발생했습니다"); + if (!isAutoRefresh) { // 자동 새로고침일 때는 에러 토스트 표시하지 않음 + toast.error("메시지를 불러오는 중 오류가 발생했습니다"); + } } finally { - setIsLoading(false); + // 항상 로딩 상태를 해제하되, 최소 200ms는 유지하여 깜빡거림 방지 + if (!isAutoRefresh) { + setTimeout(() => { + setIsLoading(false); + }, 200); + } } }; @@ -323,8 +393,8 @@ export function VendorCommunicationDrawer({ return ( <Drawer open={open} onOpenChange={onOpenChange}> - <DrawerContent className="max-h-[85vh]"> - <DrawerHeader className="border-b"> + <DrawerContent className="max-h-[80vh] flex flex-col"> + <DrawerHeader className="border-b flex-shrink-0"> <DrawerTitle className="flex items-center gap-2"> <Avatar className="h-8 w-8"> <AvatarFallback className="bg-primary/10"> @@ -341,10 +411,10 @@ export function VendorCommunicationDrawer({ </DrawerDescription> </DrawerHeader> - <div className="p-0 flex flex-col h-[60vh]"> + <div className="flex flex-col flex-1 min-h-0"> {/* 메시지 목록 */} - <ScrollArea className="flex-1 p-4"> - {isLoading ? ( + <div className="flex-1 p-4 overflow-y-auto min-h-[300px]"> + {isLoading && comments.length === 0 ? ( <div className="flex h-full items-center justify-center"> <p className="text-muted-foreground">메시지 로딩 중...</p> </div> @@ -356,7 +426,15 @@ export function VendorCommunicationDrawer({ </div> </div> ) : ( - <div className="space-y-4"> + <div className="space-y-4 relative"> + {isLoading && ( + <div className="absolute top-0 right-0 z-10 bg-background/80 backdrop-blur-sm rounded-md px-2 py-1"> + <div className="flex items-center gap-2"> + <div className="w-2 h-2 bg-primary rounded-full animate-pulse" /> + <span className="text-xs text-muted-foreground">새로고침 중...</span> + </div> + </div> + )} {comments.map(comment => ( <div key={comment.id} @@ -436,11 +514,11 @@ export function VendorCommunicationDrawer({ <div ref={messagesEndRef} /> </div> )} - </ScrollArea> + </div> {/* 선택된 첨부파일 표시 */} {attachments.length > 0 && ( - <div className="p-2 bg-muted mx-4 rounded-md mb-2"> + <div className="p-2 bg-muted mx-4 rounded-md mb-2 flex-shrink-0"> <div className="text-xs font-medium mb-1">첨부파일</div> <div className="flex flex-wrap gap-2"> {attachments.map((file, index) => ( @@ -466,7 +544,7 @@ export function VendorCommunicationDrawer({ )} {/* 메시지 입력 영역 */} - <div className="p-4 border-t"> + <div className="p-4 border-t flex-shrink-0"> <div className="flex gap-2 items-end"> <div className="flex-1"> <Textarea @@ -503,11 +581,31 @@ export function VendorCommunicationDrawer({ </div> </div> - <DrawerFooter className="border-t"> - <div className="flex justify-between"> - <Button variant="outline" onClick={() => loadComments()}> - 새로고침 - </Button> + <DrawerFooter className="border-t flex-shrink-0"> + <div className="flex justify-between items-center"> + <div className="flex items-center gap-2"> + <Button variant="outline" onClick={() => loadComments()}> + 새로고침 + </Button> + <Button + variant={autoRefresh ? "default" : "outline"} + size="sm" + onClick={toggleAutoRefresh} + className="gap-2" + > + {autoRefresh ? ( + <> + <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" /> + 자동 새로고침 ON + </> + ) : ( + <> + <div className="w-2 h-2 bg-gray-400 rounded-full" /> + 자동 새로고침 OFF + </> + )} + </Button> + </div> <DrawerClose asChild> <Button variant="outline">닫기</Button> </DrawerClose> |
