Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<string, string>>(new Map());
const objectUrlsRef = useRef<string[]>([]);
const queryClient = useQueryClient();

const scrollToBottom = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;

container.scrollTop = container.scrollHeight;
}, []);

// --- 2. 하위 Hooks 호출 ---

// API를 통해 채팅 기록을 페이지 단위로 가져옵니다.
Expand Down Expand Up @@ -173,6 +181,7 @@ const useChatListHandler = (chatId: number) => {
prevChatIdRef.current = chatId;
hasInitialAutoScrolledRef.current = false;
prevMessageCountRef.current = 0;
shouldForceScrollToBottomRef.current = false;
}, [chatId]);

// 초기 히스토리 로딩 완료 후, 최초 1회만 하단으로 이동합니다.
Expand All @@ -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;

Expand All @@ -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 함수 ---

Expand All @@ -246,12 +252,14 @@ const useChatListHandler = (chatId: number) => {
destination: `/publish/chat/${chatId}`,
body: JSON.stringify({ content, senderId }),
});
shouldForceScrollToBottomRef.current = true;
requestAnimationFrame(scrollToBottom);
Comment on lines +255 to +256
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear force-scroll state when outbound send is not confirmed

Setting shouldForceScrollToBottomRef.current = true during sendTextMessage leaves a sticky force-scroll state until the message list length increases. If the publish call succeeds locally but no message is appended (e.g., transient disconnect or server-side rejection), the next unrelated incoming message will still force-jump the user to bottom even when they are intentionally reading older history. This regression is introduced by the new force-scroll path and should be reset when no corresponding message append occurs.

Useful? React with 👍 / 👎.

invalidateChatPreviewQueries();
} else {
// 여기에 메시지 전송 실패에 대한 UI 피드백 로직을 추가할 수 있습니다. (e.g., alert, toast)
}
},
[chatId, connectionStatus, invalidateChatPreviewQueries],
[chatId, connectionStatus, invalidateChatPreviewQueries, scrollToBottom],
); // chatId와 connectionStatus가 변경될 경우에만 함수를 재생성

const sendImageMessage = useCallback(
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,14 @@ const ChatMessageBox = ({
};

return isMine ? (
<div className="flex justify-end">
<div className="flex max-w-xs flex-row-reverse gap-2">
<div className="flex flex-col items-end">
<div className="flex items-end gap-1">
<span className="text-k-500 typo-regular-4">{formatTime(message.createdAt)}</span>
<div className="rounded-b-xl rounded-tl-xl bg-primary px-3 py-2 text-white">
<div className="flex min-w-0 justify-end">
<div className="flex max-w-[min(80%,24rem)] min-w-0 flex-row-reverse gap-2">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid capping own-message row at 80%

max-w-[min(80%,24rem)] on the my-message container makes the bubble too narrow when an attachment image is present, because ChatImage is fixed at w-[200px] and the timestamp is also shrink-0; after subtracting timestamp/gap, the bubble can drop below 200px on common narrow layouts (e.g., ~360px mobile viewport once chat paddings are applied), causing horizontal overflow/compression that did not happen with the previous full-available-width behavior.

Useful? React with 👍 / 👎.

<div className="flex min-w-0 flex-col items-end">
<div className="flex min-w-0 items-end gap-1">
<span className="shrink-0 text-k-500 typo-regular-4">{formatTime(message.createdAt)}</span>
<div className="min-w-0 rounded-b-xl rounded-tl-xl bg-primary px-3 py-2 text-white">
{shouldShowContent(messageType) && (
<p className="whitespace-pre-line typo-regular-2">{message.content}</p>
<p className="whitespace-pre-line break-words typo-regular-2">{message.content}</p>
)}
{renderAttachments()}
</div>
Expand All @@ -196,19 +196,19 @@ const ChatMessageBox = ({
</div>
</div>
) : (
<div className="flex justify-start">
<div className="flex max-w-xs flex-row gap-2">
<div className="flex min-w-0 justify-start">
<div className="flex max-w-[min(100%,28rem)] min-w-0 flex-row gap-2">
<ProfileWithBadge isMentor={isPartnerMentor} width={32} height={32} />
<div className="flex flex-col items-start">
<div className="flex min-w-0 flex-col items-start">
<span className="mb-1 text-k-900 typo-medium-5">{partnerNickname}</span>
<div className="flex items-end gap-1">
<div className="rounded-b-xl rounded-tr-xl bg-k-100 px-3 py-2 text-k-900">
<div className="flex min-w-0 items-end gap-1">
<div className="min-w-0 rounded-b-xl rounded-tr-xl bg-k-100 px-3 py-2 text-k-900">
{shouldShowContent(messageType) && (
<p className="whitespace-pre-line typo-regular-2">{message.content}</p>
<p className="whitespace-pre-line break-words typo-regular-2">{message.content}</p>
)}
{renderAttachments()}
</div>
<span className="text-k-500 typo-regular-4">{formatTime(message.createdAt)}</span>
<span className="shrink-0 text-k-500 typo-regular-4">{formatTime(message.createdAt)}</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/ProfileWithBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ProfileWithBadge = ({
const iconSize = Math.round(badgeSize * 0.67);

return (
<div className="relative" style={{ width: `${width}px`, height: `${height}px` }}>
<div className="relative shrink-0" style={{ width: `${width}px`, height: `${height}px` }}>
{/* 프로필 이미지 */}
<div
className={`h-full w-full overflow-hidden rounded-full ${
Expand Down
Loading