diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts index 9187e38f..bdd37c17 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts @@ -37,10 +37,18 @@ const useChatListHandler = (chatId: number) => { const hasInitialAutoScrolledRef = useRef(false); const prevMessageCountRef = useRef(0); const prevChatIdRef = useRef(chatId); + const shouldForceScrollToBottomRef = useRef(false); const imagePreviewByUrlRef = useRef>(new Map()); const objectUrlsRef = useRef([]); const queryClient = useQueryClient(); + const scrollToBottom = useCallback(() => { + const container = scrollContainerRef.current; + if (!container) return; + + container.scrollTop = container.scrollHeight; + }, []); + // --- 2. 하위 Hooks 호출 --- // API를 통해 채팅 기록을 페이지 단위로 가져옵니다. @@ -173,6 +181,7 @@ const useChatListHandler = (chatId: number) => { prevChatIdRef.current = chatId; hasInitialAutoScrolledRef.current = false; prevMessageCountRef.current = 0; + shouldForceScrollToBottomRef.current = false; }, [chatId]); // 초기 히스토리 로딩 완료 후, 최초 1회만 하단으로 이동합니다. @@ -182,18 +191,15 @@ const useChatListHandler = (chatId: number) => { } const rafId = requestAnimationFrame(() => { - const container = scrollContainerRef.current; - if (!container) return; - - container.scrollTop = container.scrollHeight; + scrollToBottom(); hasInitialAutoScrolledRef.current = true; prevMessageCountRef.current = submittedMessages.length; }); return () => cancelAnimationFrame(rafId); - }, [isLoading, isFetchingNextPage, submittedMessages.length]); + }, [isLoading, isFetchingNextPage, scrollToBottom, submittedMessages.length]); - // 신규 메시지 도착 시, 사용자가 하단 근처에 있을 때만 자동으로 하단을 유지합니다. + // 신규 메시지 도착 시 하단 근처라면 유지하고, 내가 보낸 메시지는 현재 위치와 무관하게 하단으로 이동합니다. useEffect(() => { if (isLoading || isFetchingNextPage) return; @@ -217,21 +223,21 @@ const useChatListHandler = (chatId: number) => { } const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; + const shouldForceScrollToBottom = shouldForceScrollToBottomRef.current; - if (distanceFromBottom <= BOTTOM_PROXIMITY_THRESHOLD) { + if (shouldForceScrollToBottom || distanceFromBottom <= BOTTOM_PROXIMITY_THRESHOLD) { const rafId = requestAnimationFrame(() => { - const target = scrollContainerRef.current; - if (!target) return; - target.scrollTop = target.scrollHeight; + scrollToBottom(); }); + shouldForceScrollToBottomRef.current = false; prevMessageCountRef.current = currentMessageCount; return () => cancelAnimationFrame(rafId); } prevMessageCountRef.current = currentMessageCount; - }, [isLoading, isFetchingNextPage, submittedMessages.length]); + }, [isLoading, isFetchingNextPage, scrollToBottom, submittedMessages.length]); // --- 4. Handler 함수 --- @@ -246,12 +252,14 @@ const useChatListHandler = (chatId: number) => { destination: `/publish/chat/${chatId}`, body: JSON.stringify({ content, senderId }), }); + shouldForceScrollToBottomRef.current = true; + requestAnimationFrame(scrollToBottom); invalidateChatPreviewQueries(); } else { // 여기에 메시지 전송 실패에 대한 UI 피드백 로직을 추가할 수 있습니다. (e.g., alert, toast) } }, - [chatId, connectionStatus, invalidateChatPreviewQueries], + [chatId, connectionStatus, invalidateChatPreviewQueries, scrollToBottom], ); // chatId와 connectionStatus가 변경될 경우에만 함수를 재생성 const sendImageMessage = useCallback( @@ -312,7 +320,10 @@ const useChatListHandler = (chatId: number) => { }); } }); - if (newMessages.length > 0) setSubmittedMessages((prev) => [...prev, ...newMessages]); + if (newMessages.length > 0) { + shouldForceScrollToBottomRef.current = true; + setSubmittedMessages((prev) => [...prev, ...newMessages]); + } return previewUrls; }, [setSubmittedMessages], @@ -366,7 +377,10 @@ const useChatListHandler = (chatId: number) => { }); } }); - if (newMessages.length > 0) setSubmittedMessages((prev) => [...prev, ...newMessages]); + if (newMessages.length > 0) { + shouldForceScrollToBottomRef.current = true; + setSubmittedMessages((prev) => [...prev, ...newMessages]); + } }, [setSubmittedMessages], ); diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx index 182a9bf8..e749e580 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx @@ -180,14 +180,14 @@ const ChatMessageBox = ({ }; return isMine ? ( -
-
-
-
- {formatTime(message.createdAt)} -
+
+
+
+
+ {formatTime(message.createdAt)} +
{shouldShowContent(messageType) && ( -

{message.content}

+

{message.content}

)} {renderAttachments()}
@@ -196,19 +196,19 @@ const ChatMessageBox = ({
) : ( -
-
+
+
-
+
{partnerNickname} -
-
+
+
{shouldShowContent(messageType) && ( -

{message.content}

+

{message.content}

)} {renderAttachments()}
- {formatTime(message.createdAt)} + {formatTime(message.createdAt)}
diff --git a/apps/web/src/components/ui/ProfileWithBadge.tsx b/apps/web/src/components/ui/ProfileWithBadge.tsx index 48528a05..e0d7ffda 100644 --- a/apps/web/src/components/ui/ProfileWithBadge.tsx +++ b/apps/web/src/components/ui/ProfileWithBadge.tsx @@ -26,7 +26,7 @@ const ProfileWithBadge = ({ const iconSize = Math.round(badgeSize * 0.67); return ( -
+
{/* 프로필 이미지 */}