Skip to content

Commit f22406c

Browse files
authored
[feat] 추천 챗봇 페이지 자동 스크롤 구현 (#91)
* [fix] 환영팝업 이미지 경고 수정 * [feat] 자동 스크롤 기능 구현 * [style] 새메세지 알림버튼 style 수정
1 parent bc7c98f commit f22406c

File tree

9 files changed

+114
-38
lines changed

9 files changed

+114
-38
lines changed

src/domains/login/components/WelcomeModal.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ function Welcome({ userNickname, open, onClose }: Props) {
2020
<ModalLayout
2121
open={open}
2222
onClose={onClose}
23-
title={`환영합니다, ${userNickname}님!`}
24-
description="바텐더 쑤리가 안내해드릴게요"
23+
title={`환영합니다! `}
24+
description={
25+
<span className="block text-center">
26+
{userNickname}<br /> 바텐더 쑤리가 안내해드릴게요
27+
</span>
28+
}
2529
buttons={
2630
<>
2731
<Button
@@ -48,7 +52,7 @@ function Welcome({ userNickname, open, onClose }: Props) {
4852
>
4953
<div className="flex-center">
5054
<div className="relative w-32 h-32" aria-hidden>
51-
<Image src={Ssury} alt="" fill sizes="128px" className="object-contain" />
55+
<Image src={Ssury} alt="" fill className="object-contain" sizes="8rem" priority />
5256
</div>
5357
</div>
5458
</ModalLayout>

src/domains/recommend/components/ChatSection.tsx

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,87 @@
11
'use client';
22

3-
import { useState } from 'react';
4-
import BotMessage from './BotMessage';
5-
import UserMessage from './UserMessage';
6-
import MessageInput from './MessageInput';
3+
import { useEffect, useRef, useState } from 'react';
4+
import BotMessage from './bot/BotMessage';
5+
import UserMessage from './user/UserMessage';
6+
import NewMessageAlert from './bot/NewMessageAlert';
7+
import MessageInput from './user/MessageInput';
8+
9+
// TODOS : 아직 api 몰라서 임시 type
10+
interface ChatMessage {
11+
id: number;
12+
message: string;
13+
sender: 'user' | 'bot';
14+
}
715

816
function ChatSection() {
9-
const [messages, setMessages] = useState<string[]>([]);
17+
const [messages, setMessages] = useState<ChatMessage[]>([]);
18+
const chatEndRef = useRef<HTMLDivElement>(null);
19+
const chatListRef = useRef<HTMLDivElement>(null);
20+
const isScrollBottom = useRef(true);
21+
const [showNewMessageAlert, setShowNewMessageAlert] = useState(false);
1022

1123
const handleSubmit = (message: string) => {
12-
setMessages((prev) => [...prev, message]);
24+
// 사용자 메시지
25+
setMessages((prev) => [...prev, { id: prev.length + 1, message, sender: 'user' }]);
26+
};
27+
28+
// 쑤리 임시 메시지
29+
// useEffect(() => {
30+
// const interval = setInterval(() => {
31+
// setMessages((prev) => [
32+
// ...prev,
33+
// { id: prev.length + 1, message: `새 메시지 ${prev.length + 1}`, sender: 'bot' },
34+
// ]);
35+
// }, 1000);
36+
37+
// return () => clearInterval(interval);
38+
// }, []);
39+
40+
// 스크롤 제일 아래인지 체크
41+
const handleCheckBottom = (e: React.UIEvent<HTMLDivElement>) => {
42+
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
43+
44+
isScrollBottom.current = scrollTop + clientHeight >= scrollHeight - 10;
45+
46+
if (isScrollBottom.current) setShowNewMessageAlert(false);
47+
};
48+
49+
// 새 메시지가 들어오면 자동 스크롤
50+
useEffect(() => {
51+
if (isScrollBottom.current) {
52+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
53+
setShowNewMessageAlert(false); // 새메세지 숨김
54+
} else {
55+
setShowNewMessageAlert(true); // 새메세지 보여줌
56+
}
57+
}, [messages]);
58+
59+
// 스크롤 제일 아래로
60+
const handleScrollToBottom = () => {
61+
if (chatListRef.current) {
62+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
63+
isScrollBottom.current = true;
64+
}
1365
};
1466

1567
return (
16-
<section className="page-layout max-w-1024 py-12 ">
68+
<section className="mx-auto w-full flex-1">
1769
<h2 className="sr-only">대화 목록 및 입력 창</h2>
18-
<div className="flex flex-col gap-10 pb-20">
19-
<BotMessage />
70+
<div
71+
ref={chatListRef}
72+
onScroll={handleCheckBottom}
73+
className="flex flex-col gap-10 pt-12 px-3 overflow-y-auto max-h-[calc(100vh-116px)] md:max-h-[calc(100vh-144px)]"
74+
>
75+
{messages.map(({ id, message, sender }) =>
76+
sender === 'user' ? (
77+
<UserMessage key={id} message={message} />
78+
) : (
79+
<BotMessage key={id} messages={[{ id, type: 'text', message }]} />
80+
)
81+
)}
2082

21-
{messages.map((msg, i) => (
22-
<UserMessage key={i} message={msg} />
23-
))}
83+
<div ref={chatEndRef}></div>
84+
{showNewMessageAlert && <NewMessageAlert onClick={handleScrollToBottom} />}
2485
</div>
2586
<MessageInput onSubmit={handleSubmit} />
2687
</section>

src/domains/recommend/components/BotMessage.tsx renamed to src/domains/recommend/components/bot/BotMessage.tsx

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,26 @@ import BotOptions from './BotOptions';
88
import TypingIndicator from './TypingIndicator';
99

1010
interface Message {
11-
id: string;
12-
text?: string;
11+
id: number;
12+
message?: string;
1313
type?: 'radio' | 'text' | 'recommend';
1414
}
1515

16-
function BotMessage() {
16+
interface BotMessages {
17+
messages: Message[];
18+
isTyping?: boolean;
19+
}
20+
21+
function BotMessage({ messages, isTyping = false }: BotMessages) {
1722
const [selected, setSelected] = useState('option1');
1823

19-
// radio 옵션
24+
// 임시 radio 옵션
2025
const options = [
2126
{ label: '옵션 1', value: 'option1' },
2227
{ label: '옵션 2', value: 'option2' },
2328
{ label: '옵션 3', value: 'option3' },
2429
];
2530

26-
// 메시지 (연속 메시지)
27-
const messages: Message[] = [
28-
{
29-
id: '1',
30-
text: '안녕하세요, 바텐더 쑤리에요. \n 취향에 맞는 칵테일을 추천해드릴게요!',
31-
},
32-
{
33-
id: '2',
34-
text: '어떤 유형으로 찾아드릴까요?',
35-
type: 'radio',
36-
},
37-
{
38-
id: '3',
39-
type: 'recommend',
40-
},
41-
];
42-
4331
return (
4432
<article aria-label="취향추천 챗봇 메시지" className="">
4533
<header className="flex items-end">
@@ -73,7 +61,7 @@ function BotMessage() {
7361
</ul>
7462
) : (
7563
<div className="flex flex-col w-fit max-w-[80%] min-w-[120px] p-3 rounded-2xl rounded-tl-none bg-white text-black">
76-
{msg.text && <p className="whitespace-pre-line">{msg.text}</p>}
64+
{msg.message && <p className="whitespace-pre-line">{msg.message}</p>}
7765

7866
{/* radio */}
7967
{msg.type === 'radio' && (
@@ -83,7 +71,7 @@ function BotMessage() {
8371
)}
8472
</div>
8573
))}
86-
<TypingIndicator />
74+
{isTyping && <TypingIndicator />}
8775
</div>
8876
</article>
8977
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
import Down from '@/shared/assets/icons/selectDown_24.svg';
3+
4+
interface Props {
5+
onClick: () => void;
6+
}
7+
8+
function NewMessageAlert({ onClick }: Props) {
9+
return (
10+
<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)]
13+
hover:bg-tertiary hover:text-white active:bg-tertiary active:text-white
14+
transition-colors duration-200 ease-in-out"
15+
onClick={onClick}
16+
aria-label="새 메시지로 이동"
17+
>
18+
<span className="text-sm">새 메시지</span>
19+
<Down className="w-6 h-6 text-inherit" />
20+
</button>
21+
);
22+
}
23+
export default NewMessageAlert;

0 commit comments

Comments
 (0)