diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index c3119ddc..b223ba3a 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { MessageSquare, Pencil, Check, X, ChevronsDown } from "lucide-react"; import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger"; import { useAuth } from "@/hooks/useAuth"; @@ -16,14 +16,14 @@ interface ChatPanelProps { export function ChatPanel({ room }: ChatPanelProps) { const { user } = useAuth(); - const { selectedRoomId } = useMessengerContext(); + const { selectedRoomId, isOpen } = useMessengerContext(); const { data: messages } = useMessages(selectedRoomId); const markAsRead = useMarkAsRead(); const updateRoom = useUpdateRoom(); const { emitTypingStart, emitTypingStop, typingUsers } = useMessengerSocket(); const scrollRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); - const [scrollReady, setScrollReady] = useState(false); + const bottomRef = useRef(null); const [isEditingName, setIsEditingName] = useState(false); const [editName, setEditName] = useState(""); const editInputRef = useRef(null); @@ -36,34 +36,41 @@ export function ChatPanel({ room }: ChatPanelProps) { const lastMessageId = messages?.[messages.length - 1]?.id; - // Hide messages until scrolled to bottom (prevents visible jump on room open) - useEffect(() => { - setScrollReady(false); - }, [selectedRoomId]); + // Scroll to bottom: sentinel scrollIntoView before paint (no visible jump) + // Scroll to bottom on room open / new message + useLayoutEffect(() => { + if (isOpen) bottomRef.current?.scrollIntoView(); + }, [selectedRoomId, lastMessageId, isOpen]); - // Scroll to bottom when messages load or room changes - // Two-pass: immediate rAF + 600ms delayed (for async image loads) + // ResizeObserver: re-scroll whenever content height changes (images loading, etc.) + const shouldAutoScrollRef = useRef(true); useEffect(() => { - requestAnimationFrame(() => { - const el = scrollRef.current; - if (el) { - el.scrollTop = el.scrollHeight; - setScrollReady(true); + const el = scrollRef.current; + if (!el) return; + const inner = el.firstElementChild as HTMLElement | null; + if (!inner) return; + const ro = new ResizeObserver(() => { + if (shouldAutoScrollRef.current) { + bottomRef.current?.scrollIntoView(); } }); - const t = setTimeout(() => { - const el = scrollRef.current; - if (el) el.scrollTop = el.scrollHeight; - }, 600); - return () => clearTimeout(t); - }, [lastMessageId, selectedRoomId]); + ro.observe(inner); + return () => ro.disconnect(); + }, [selectedRoomId]); + + // Track whether user has scrolled up (disable auto-scroll while reading old messages) + useEffect(() => { + shouldAutoScrollRef.current = true; + }, [selectedRoomId, lastMessageId]); // Re-attach scroll listener whenever room changes (scrollRef mounts after room is set) useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { - setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 60); + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60; + setIsAtBottom(atBottom); + shouldAutoScrollRef.current = atBottom; }; el.addEventListener("scroll", onScroll); return () => el.removeEventListener("scroll", onScroll); @@ -170,7 +177,7 @@ export function ChatPanel({ room }: ChatPanelProps) { {/* Messages */} -
+
{messages?.map((msg, idx) => (
@@ -199,6 +206,7 @@ export function ChatPanel({ room }: ChatPanelProps) {
{roomTyping && roomTyping.length > 0 ? `${roomTyping.join(", ")}님이 입력 중...` : ""}
+