diff --git a/src/components/ChattingDetail.jsx b/src/components/ChattingDetail.jsx index fb65a956967c16dcd847f76500a2c62ede644dfa..318a02a050ddfae706414083384036b38cba6f4b 100644 --- a/src/components/ChattingDetail.jsx +++ b/src/components/ChattingDetail.jsx @@ -727,7 +727,7 @@ function ChattingDetail() { // 공지사항 목록 가져오기 const fetchNotices = async () => { try { - const response = await fetch(`http://localhost:8080/api/chat/${chatRoomId}/notices`); + const response = await fetch(`${process.env.REACT_APP_BASE_URL}/api/chat/${chatRoomId}/notices`); if (response.ok) { const data = await response.json(); setNotices(data); // 공지사항 목록 업데이트 @@ -767,6 +767,28 @@ function ChattingDetail() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [chatRoomId]); // chatRoomId 변경 또는 컴포넌트 언마운트 시 실행 + useEffect(() => { + const lastMessage = messages[messages.length - 1]; + + localStorage.setItem( + `loadedPreviousMessages-${chatRoomId}`, + "true" + ); + + const hasLoadedPreviousMessages = localStorage.getItem( + `loadedPreviousMessages-${chatRoomId}` + ); + + if (!hasLoadedPreviousMessages && lastMessage && lastMessage.type === "previousMessages") { + return; // 이전 메시지일 경우 자동 스크롤 방지 + } + + // 새 메시지 수신 시 자동으로 아래로 스크롤 + if (lastMessage && lastMessage.type === "message" && messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages, chatRoomId]); + const highlightSearchTerm = (message) => { if (!searchTerm) return message; @@ -792,11 +814,9 @@ function ChattingDetail() { <Button size="lg" theme="pink" - icon={<FaSearch />} + icon={<FaSearch />} onClick={toggleSearchBar} - > - - </Button> + ></Button> </ChatRoomHeader> {/* 공지사항 */} @@ -804,12 +824,13 @@ function ChattingDetail() { <NoticeContainer isCollapsed={isNoticeCollapsed}> {!isNoticeCollapsed ? ( <> - <NoticeMessage isCollapsed={isNoticeCollapsed} onClick={handleNoticeClick}> + <NoticeMessage + isCollapsed={isNoticeCollapsed} + onClick={handleNoticeClick} + > 📢 {notice?.message} </NoticeMessage> - <NoticeSender> - {notice?.sender} - </NoticeSender> + <NoticeSender>{notice?.sender}</NoticeSender> <NoticeActions> <button onClick={handleNoticeCollapse}>접어두기</button> <button onClick={handleDismissNotice}>다시 열지 않음</button> @@ -836,7 +857,7 @@ function ChattingDetail() { {/* 공지사항 상세 모달 */} {isNoticeDetailModalOpen && ( <ChattingNoticeDetailModal - initialNotice={selectedNotice} // 처음 표시할 공지사항 + initialNotice={selectedNotice} notices={notices} onClose={closeNoticeDetailModal} onSelectNotice={(notice) => setSelectedNotice(notice)} @@ -856,78 +877,196 @@ function ChattingDetail() { )} {isSearching && ( - <FixedSearchBar> - <input - ref={searchInputRef} - type="text" - value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} - onKeyPress={handleSearch} - placeholder="대화 내용 검색" - /> - <div className="arrow-buttons"> - <FaArrowUp - onClick={handleArrowUp} - size={20} - style={{ pointerEvents: currentSearchIndex > 0 ? 'auto' : 'none', opacity: currentSearchIndex > 0 ? 1 : 0.5 }} + <FixedSearchBar> + <input + ref={searchInputRef} + type="text" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + onKeyPress={handleSearch} + placeholder="대화 내용 검색" + className="flex-[1.2] px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 mr-3" /> - <FaArrowDown - onClick={handleArrowDown} - size={20} - style={{ - pointerEvents: currentSearchIndex < searchResults.length - 1 ? 'auto' : 'none', - opacity: currentSearchIndex < searchResults.length - 1 ? 1 : 0.5, - }} - /> - </div> - {searchResults.length === 0 && ( - <p style={{ color: 'gray', fontSize: '0.9em', textAlign: 'center', marginTop: '10px' }}> - 더 이상 검색 결과가 없습니다. - </p> - )} - </FixedSearchBar> - )} + <div className="flex items-center space-x-1"> + <Button + size="sm" + theme="purple" + onClick={handleArrowUp} + state={currentSearchIndex > 0 ? "default" : "disable"} + icon={<FaArrowUp />} + /> + <Button + size="sm" + theme="purple" + onClick={handleArrowDown} + state={ + currentSearchIndex < searchResults.length - 1 + ? "default" + : "disable" + } + icon={<FaArrowDown />} + /> + <Button + size="sm" + theme="black" + onClick={() => { + setSearchTerm(""); + setSearchResults([]); + setCurrentSearchIndex(0); + setIsSearching(false); + }} + > + 닫기 + </Button> + </div> + </FixedSearchBar> + )} <ChatRoomMessages> {messages.length === 0 ? ( <p>메시지가 없습니다.</p> ) : ( messages.map((messageData, index) => { + const isMine = messageData.sender === loggedInUser; + const prevMessage = messages[index - 1]; + const nextMessage = messages[index + 1]; + + const sameSenderAsPrev = + prevMessage && prevMessage.sender === messageData.sender; + + const sameSenderAsNext = + nextMessage && nextMessage.sender === messageData.sender; + + const messageTime = new Date(messageData.timestamp).toLocaleTimeString( + [], + { + hour: "2-digit", + minute: "2-digit", + } + ); + + const prevMessageTime = + prevMessage && + new Date(prevMessage.timestamp).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + + // 새로운 분 단위 메시지인지 확인 + const isNewMinute = !prevMessage || messageTime !== prevMessageTime; + const isLastMessageInGroup = !sameSenderAsNext; + const isDifferentUserFromPrev = !sameSenderAsPrev; + if (messageData.type === "join" || messageData.type === "leave") { return ( <CenteredMessage key={index}>{messageData.message}</CenteredMessage> ); } + const unreadCountValue = unreadCount(messageData._id); + return ( - <MessageContainer - key={index} - isMine={messageData.sender === loggedInUser} - highlighted={searchResults[currentSearchIndex] === messageData} - ref={ - searchResults[currentSearchIndex] === messageData - ? highlightedMessageRef - : null - } - > - <MessageBubble - isMine={messageData.sender === loggedInUser} - onContextMenu={(e) => handleRightClick(e, messageData)} - > - <div> - {messageData.sender !== loggedInUser && ( - <strong>{messageData.sender}</strong> + <div key={index}> + {/* 이전 사용자와 다르거나 분이 달라지면 이름 표시 */} + {isDifferentUserFromPrev && !isMine && ( + <strong + style={{ + display: "block", + marginBottom: "-12px", + fontSize: "0.9em", + color: "#555", + }} + > + {messageData.sender} + </strong> + )} + + <MessageContainer + isMine={isMine} + highlighted={searchResults[currentSearchIndex] === messageData} + ref={ + searchResults[currentSearchIndex] === messageData + ? highlightedMessageRef + : null + } + style={{ + marginTop: sameSenderAsPrev ? "-16px" : "8px", + justifyContent: isMine ? "flex-end" : "flex-start", + alignItems: "center", // 중앙 정렬 + }} + > + {/* 내가 보낸 메시지: 읽지 않은 사람 수는 왼쪽에 표시 */} + {isMine && unreadCountValue > 0 && ( + <div + style={{ + fontSize: "0.8em", + color: "gray", + marginRight: "8px", // 메시지 버블과 간격 + textAlign: "center", + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: "#f0f0f0", + borderRadius: "50%", + width: "20px", + height: "20px", + boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.1)", + }} + > + {unreadCountValue} + </div> + )} + + <MessageBubble + isMine={isMine} + highlighted={searchResults[currentSearchIndex] === messageData} + onContextMenu={(e) => handleRightClick(e, messageData)} + style={{ + textAlign: "left", + maxWidth: "75%", // 화면의 75%를 넘지 않도록 제한 + }} + > + <div + style={{ + display: "flex", + alignItems: "center", + justifyContent: isMine ? "flex-end" : "flex-start", + }} + > + {highlightSearchTerm(messageData.message)} + </div> + + {/* 메시지의 하단 시간 표시 */} + {(isNewMinute || isLastMessageInGroup) && ( + <MessageTimestamp isMine={isMine}> + {messageTime} + </MessageTimestamp> )} - {highlightSearchTerm(messageData.message)} - </div> - <MessageTimestamp isMine={messageData.sender === loggedInUser}> - {new Date(messageData.timestamp).toLocaleTimeString()} - <span>{` (${unreadCount( - messageData._id - )}명이 읽지 않음)`}</span> - </MessageTimestamp> - </MessageBubble> - </MessageContainer> + </MessageBubble> + + {/* 상대방이 보낸 메시지: 읽지 않은 사람 수는 오른쪽에 표시 */} + {!isMine && unreadCountValue > 0 && ( + <div + style={{ + fontSize: "0.8em", + color: "gray", + marginLeft: "8px", // 메시지 버블과 간격 + textAlign: "center", + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: "#f0f0f0", + borderRadius: "50%", + width: "20px", + height: "20px", + boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.1)", + }} + > + {unreadCountValue} + </div> + )} + </MessageContainer> + </div> ); }) )} @@ -941,8 +1080,17 @@ function ChattingDetail() { onChange={(e) => setInput(e.target.value)} placeholder="메시지를 입력하세요" onKeyPress={(e) => e.key === "Enter" && sendMessage()} + className="flex-1 px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" /> - <button onClick={sendMessage}>전송</button> + <Button + size="lg" + theme="pink" + state={input.trim() ? "default" : "disable"} + onClick={sendMessage} + className="ml-3 flex items-center justify-center whitespace-nowrap w-16" // w-32로 버튼 너비 설정 + > + 전송 + </Button> </ChatRoomInput> </ChatRoomContainer> );