Skip to content

Commit 76c6c2b

Browse files
authored
[fix] 챗봇 추천 페이지 단계별 일 시 자동 scroll 이슈 (#103)
* [feat] 로딩 , fetch 관리 함수 * [style] 레이아웃 수정 * [refactor] scroll 관련 chatlist로 이동 * [feat] input 백그라운드 수정 * [fix] cancelanimation 추가
1 parent 2aa6999 commit 76c6c2b

File tree

7 files changed

+55
-62
lines changed

7 files changed

+55
-62
lines changed

src/app/recommend/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Bg from '@/shared/assets/images/recommend_bg.webp';
44
function Page() {
55
return (
66
<div
7-
className="relative bg-repeat bg-auto w-full flex flex-col overflow-hidden"
7+
className="relative bg-repeat bg-auto w-full flex flex-col"
88
style={{ backgroundImage: `url(${Bg.src})` }}
99
>
1010
<h1 className="sr-only">취향추천하기</h1>

src/domains/recommend/components/ChatList.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useChatScroll } from '../hook/useChatScroll';
12
import { ChatListProps } from '../types/recommend';
23
import BotMessage from './bot/BotMessage';
34
import NewMessageAlert from './bot/NewMessageAlert';
@@ -9,24 +10,19 @@ function ChatList({
910
userCurrentStep,
1011
onSelectedOption,
1112
getRecommendations,
12-
chatListRef,
13-
chatEndRef,
14-
showNewMessageAlert,
15-
handleCheckBottom,
16-
handleScrollToBottom,
1713
isBotTyping,
1814
}: ChatListProps) {
15+
const { chatListRef, chatEndRef, showNewMessageAlert, handleCheckBottom, handleScrollToBottom } =
16+
useChatScroll(messages[messages.length - 1]?.id);
17+
1918
return (
2019
<div
2120
ref={chatListRef}
2221
onScroll={handleCheckBottom}
23-
className="absolute top-0 left-0 right-0 bottom-20 w-full gap-5 px-3 pt-12 pb-5 flex flex-col items-center overflow-y-auto pr-2"
22+
className="absolute top-0 left-0 bottom-18 sm:bottom-21 w-full gap-5 px-3 pt-12 pb-4 flex flex-col items-center overflow-y-auto pr-2"
2423
>
2524
<div className="max-w-1024 w-full">
26-
{messages.map((msg, idx) => {
27-
const isLastMessage = idx === messages.length - 1;
28-
const showTyping = isLastMessage && msg.sender === 'CHATBOT' && isBotTyping;
29-
25+
{messages.map((msg) => {
3026
if (msg.sender === 'USER') {
3127
return <UserMessage key={`${msg.id}-${msg.sender}`} message={msg.message} />;
3228
}

src/domains/recommend/components/ChatSection.tsx

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
'use client';
22

33
import { useEffect, useRef, useState } from 'react';
4-
import BotMessage from './bot/BotMessage';
5-
import UserMessage from './user/UserMessage';
6-
import NewMessageAlert from './bot/NewMessageAlert';
74
import MessageInput from './user/MessageInput';
8-
import { useChatScroll } from '../hook/useChatScroll';
95
import {
106
fetchChatHistory,
117
fetchGreeting,
@@ -23,8 +19,7 @@ import ChatList from './ChatList';
2319

2420
function ChatSection() {
2521
const [messages, setMessages] = useState<ChatMessage[]>([]);
26-
const { chatListRef, chatEndRef, showNewMessageAlert, handleCheckBottom, handleScrollToBottom } =
27-
useChatScroll(messages.length);
22+
2823
const [userCurrentStep, setUserCurrentStep] = useState(0);
2924
const [isBotTyping, setIsBotTyping] = useState(false);
3025

@@ -34,6 +29,33 @@ function ChatSection() {
3429
selectedCocktailType?: string;
3530
}>({});
3631

32+
const handleSendMessage = async (payload: stepPayload | { message: string; userId: string }) => {
33+
const typingTimer = setTimeout(() => setIsBotTyping(true), 300);
34+
35+
try {
36+
if (!('currentStep' in payload)) {
37+
const botMessage = await fetchSendTextMessage(payload);
38+
clearTimeout(typingTimer);
39+
setIsBotTyping(false);
40+
41+
if (!botMessage) return;
42+
setTimeout(() => setMessages((prev) => [...prev, botMessage]), 500);
43+
return;
44+
}
45+
46+
const botMessage = await fetchSendStepMessage(payload);
47+
clearTimeout(typingTimer);
48+
setIsBotTyping(false);
49+
50+
if (!botMessage) return;
51+
setTimeout(() => setMessages((prev) => [...prev, botMessage]), 500);
52+
} catch (err) {
53+
clearTimeout(typingTimer);
54+
setIsBotTyping(false);
55+
console.error(err);
56+
}
57+
};
58+
3759
// 일반 텍스트 보낼 시
3860
const handleSubmitText = async (message: string) => {
3961
const userId = useAuthStore.getState().user?.id;
@@ -48,8 +70,7 @@ function ChatSection() {
4870
{ id: tempId, userId, message, sender: 'USER', type: 'text', createdAt: tempCreatedAt },
4971
]);
5072

51-
const botMessage = await fetchSendTextMessage({ message, userId });
52-
if (botMessage) setMessages((prev) => [...prev, botMessage]);
73+
await handleSendMessage({ message, userId });
5374
};
5475

5576
// 옵션 클릭 시
@@ -92,9 +113,6 @@ function ChatSection() {
92113
case 3:
93114
selectedOptions.current.selectedAlcoholBaseType = value;
94115
break;
95-
case 4:
96-
selectedOptions.current.selectedCocktailType = value;
97-
break;
98116
}
99117

100118
const payload: stepPayload = {
@@ -104,22 +122,7 @@ function ChatSection() {
104122
...selectedOptions.current,
105123
};
106124

107-
const typingTimer = setTimeout(() => setIsBotTyping(true), 300);
108-
109-
try {
110-
const botMessage = await fetchSendStepMessage(payload);
111-
112-
clearTimeout(typingTimer);
113-
setIsBotTyping(false);
114-
115-
if (botMessage) {
116-
setMessages((prev) => [...prev, botMessage]);
117-
}
118-
} catch (err) {
119-
clearTimeout(typingTimer);
120-
setIsBotTyping(false);
121-
console.error(err);
122-
}
125+
await handleSendMessage(payload);
123126
};
124127

125128
// 채팅 기록 불러오기 없으면 greeting 호출
@@ -145,18 +148,13 @@ function ChatSection() {
145148
};
146149

147150
return (
148-
<section className="relative flex-1 flex flex-col w-fulloverflow-hidden">
151+
<section className="relative flex-1 flex flex-col items-center w-full">
149152
<h2 className="sr-only">대화 목록 및 입력 창</h2>
150153
<ChatList
151154
messages={messages}
152155
userCurrentStep={userCurrentStep}
153156
onSelectedOption={handleSelectedOption}
154157
getRecommendations={getRecommendations}
155-
chatListRef={chatListRef}
156-
chatEndRef={chatEndRef}
157-
showNewMessageAlert={showNewMessageAlert}
158-
handleCheckBottom={handleCheckBottom}
159-
handleScrollToBottom={handleScrollToBottom}
160158
isBotTyping={isBotTyping}
161159
/>
162160
<MessageInput onSubmit={handleSubmitText} />

src/domains/recommend/components/bot/NewMessageAlert.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ interface Props {
88
function NewMessageAlert({ onClick }: Props) {
99
return (
1010
<button
11-
className="absolute left-1/2 bottom-[80px] md:bottom-[6.25rem] -translate-x-1/2 flex-center gap-1
12-
w-[calc(100%-24px)] sm:w-fit bg-secondary text-primary rounded-full px-5 py-1 shadow-[0_2px_4px_rgba(255,255,255,0.3)]
11+
className="fixed left-1/2 bottom-25 -translate-x-1/2 flex-center gap-1
12+
w-[calc(100%-24px)] sm:w-fit bg-secondary text-primary rounded-full px-5 py-1 shadow-[0_2px_4px_rgba(0,0,0,0.4)]
1313
hover:bg-tertiary hover:text-white active:bg-tertiary active:text-white
1414
transition-colors duration-200 ease-in-out"
1515
onClick={onClick}

src/domains/recommend/components/user/MessageInput.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ function MessageInput({ onSubmit }: Props) {
2121
};
2222

2323
return (
24-
<div className="fixed left-1/2 bottom-0 -translate-x-1/2 w-full max-w-[64rem] px-3 py-4 bg-primary">
25-
<form onSubmit={(e) => e.preventDefault()}>
26-
<div className="flex items-end w-full gap-2">
24+
<div className="fixed left-0 bottom-0 w-full px-3 py-4 flex-center bg-primary">
25+
<form onSubmit={(e) => e.preventDefault()} className="w-full max-w-[64rem]">
26+
<div className="flex items-end gap-2">
2727
<label htmlFor="chatInput" className="sr-only">
2828
질문 입력창
2929
</label>

src/domains/recommend/hook/useChatScroll.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useRef, useState } from 'react';
22

3-
export const useChatScroll = (messagesLength: number) => {
3+
export const useChatScroll = (lastMessageId: string) => {
44
const chatEndRef = useRef<HTMLDivElement>(null);
55
const chatListRef = useRef<HTMLDivElement>(null);
66
const isScrollBottom = useRef(true);
@@ -17,13 +17,18 @@ export const useChatScroll = (messagesLength: number) => {
1717

1818
// 새 메시지가 들어오면 자동 스크롤
1919
useEffect(() => {
20-
if (isScrollBottom.current) {
21-
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
22-
setShowNewMessageAlert(false); // 새메세지 숨김
23-
} else {
24-
setShowNewMessageAlert(true); // 새메세지 보여줌
20+
if (!isScrollBottom.current) {
21+
setShowNewMessageAlert(true);
22+
return;
2523
}
26-
}, [messagesLength]);
24+
25+
const frameId = requestAnimationFrame(() => {
26+
chatEndRef.current?.scrollIntoView({ behavior: 'auto' });
27+
setShowNewMessageAlert(false);
28+
});
29+
30+
return () => cancelAnimationFrame(frameId);
31+
}, [lastMessageId]);
2732

2833
// 스크롤 제일 아래로
2934
const handleScrollToBottom = () => {

src/domains/recommend/types/recommend.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,5 @@ export interface ChatListProps {
6060
stepData?: StepRecommendation | null
6161
) => RecommendationItem[];
6262

63-
chatListRef: React.RefObject<HTMLDivElement | null>;
64-
chatEndRef: React.RefObject<HTMLDivElement | null>;
65-
showNewMessageAlert: boolean;
66-
handleCheckBottom: (e: React.UIEvent<HTMLDivElement>) => void;
67-
handleScrollToBottom: () => void;
68-
6963
isBotTyping: boolean;
7064
}

0 commit comments

Comments
 (0)