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
2 changes: 1 addition & 1 deletion src/app/recommend/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Bg from '@/shared/assets/images/recommend_bg.webp';
function Page() {
return (
<div
className="relative bg-repeat bg-auto w-full flex flex-col overflow-hidden"
className="relative bg-repeat bg-auto w-full flex flex-col"
style={{ backgroundImage: `url(${Bg.src})` }}
>
<h1 className="sr-only">취향추천하기</h1>
Expand Down
16 changes: 6 additions & 10 deletions src/domains/recommend/components/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useChatScroll } from '../hook/useChatScroll';
import { ChatListProps } from '../types/recommend';
import BotMessage from './bot/BotMessage';
import NewMessageAlert from './bot/NewMessageAlert';
Expand All @@ -9,24 +10,19 @@ function ChatList({
userCurrentStep,
onSelectedOption,
getRecommendations,
chatListRef,
chatEndRef,
showNewMessageAlert,
handleCheckBottom,
handleScrollToBottom,
isBotTyping,
}: ChatListProps) {
const { chatListRef, chatEndRef, showNewMessageAlert, handleCheckBottom, handleScrollToBottom } =
useChatScroll(messages[messages.length - 1]?.id);

return (
<div
ref={chatListRef}
onScroll={handleCheckBottom}
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"
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"
>
<div className="max-w-1024 w-full">
{messages.map((msg, idx) => {
const isLastMessage = idx === messages.length - 1;
const showTyping = isLastMessage && msg.sender === 'CHATBOT' && isBotTyping;

{messages.map((msg) => {
if (msg.sender === 'USER') {
return <UserMessage key={`${msg.id}-${msg.sender}`} message={msg.message} />;
}
Expand Down
64 changes: 31 additions & 33 deletions src/domains/recommend/components/ChatSection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
'use client';

import { useEffect, useRef, useState } from 'react';
import BotMessage from './bot/BotMessage';
import UserMessage from './user/UserMessage';
import NewMessageAlert from './bot/NewMessageAlert';
import MessageInput from './user/MessageInput';
import { useChatScroll } from '../hook/useChatScroll';
import {
fetchChatHistory,
fetchGreeting,
Expand All @@ -23,8 +19,7 @@ import ChatList from './ChatList';

function ChatSection() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const { chatListRef, chatEndRef, showNewMessageAlert, handleCheckBottom, handleScrollToBottom } =
useChatScroll(messages.length);

const [userCurrentStep, setUserCurrentStep] = useState(0);
const [isBotTyping, setIsBotTyping] = useState(false);

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

const handleSendMessage = async (payload: stepPayload | { message: string; userId: string }) => {
const typingTimer = setTimeout(() => setIsBotTyping(true), 300);

try {
if (!('currentStep' in payload)) {
const botMessage = await fetchSendTextMessage(payload);
clearTimeout(typingTimer);
setIsBotTyping(false);

if (!botMessage) return;
setTimeout(() => setMessages((prev) => [...prev, botMessage]), 500);
return;
}

const botMessage = await fetchSendStepMessage(payload);
clearTimeout(typingTimer);
setIsBotTyping(false);

if (!botMessage) return;
setTimeout(() => setMessages((prev) => [...prev, botMessage]), 500);
} catch (err) {
clearTimeout(typingTimer);
setIsBotTyping(false);
console.error(err);
}
};

// 일반 텍스트 보낼 시
const handleSubmitText = async (message: string) => {
const userId = useAuthStore.getState().user?.id;
Expand All @@ -48,8 +70,7 @@ function ChatSection() {
{ id: tempId, userId, message, sender: 'USER', type: 'text', createdAt: tempCreatedAt },
]);

const botMessage = await fetchSendTextMessage({ message, userId });
if (botMessage) setMessages((prev) => [...prev, botMessage]);
await handleSendMessage({ message, userId });
};

// 옵션 클릭 시
Expand Down Expand Up @@ -92,9 +113,6 @@ function ChatSection() {
case 3:
selectedOptions.current.selectedAlcoholBaseType = value;
break;
case 4:
selectedOptions.current.selectedCocktailType = value;
break;
}

const payload: stepPayload = {
Expand All @@ -104,22 +122,7 @@ function ChatSection() {
...selectedOptions.current,
};

const typingTimer = setTimeout(() => setIsBotTyping(true), 300);

try {
const botMessage = await fetchSendStepMessage(payload);

clearTimeout(typingTimer);
setIsBotTyping(false);

if (botMessage) {
setMessages((prev) => [...prev, botMessage]);
}
} catch (err) {
clearTimeout(typingTimer);
setIsBotTyping(false);
console.error(err);
}
await handleSendMessage(payload);
};

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

return (
<section className="relative flex-1 flex flex-col w-fulloverflow-hidden">
<section className="relative flex-1 flex flex-col items-center w-full">
<h2 className="sr-only">대화 목록 및 입력 창</h2>
<ChatList
messages={messages}
userCurrentStep={userCurrentStep}
onSelectedOption={handleSelectedOption}
getRecommendations={getRecommendations}
chatListRef={chatListRef}
chatEndRef={chatEndRef}
showNewMessageAlert={showNewMessageAlert}
handleCheckBottom={handleCheckBottom}
handleScrollToBottom={handleScrollToBottom}
isBotTyping={isBotTyping}
/>
<MessageInput onSubmit={handleSubmitText} />
Expand Down
4 changes: 2 additions & 2 deletions src/domains/recommend/components/bot/NewMessageAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface Props {
function NewMessageAlert({ onClick }: Props) {
return (
<button
className="absolute left-1/2 bottom-[80px] md:bottom-[6.25rem] -translate-x-1/2 flex-center gap-1
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)]
className="fixed left-1/2 bottom-25 -translate-x-1/2 flex-center gap-1
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)]
hover:bg-tertiary hover:text-white active:bg-tertiary active:text-white
transition-colors duration-200 ease-in-out"
onClick={onClick}
Expand Down
6 changes: 3 additions & 3 deletions src/domains/recommend/components/user/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ function MessageInput({ onSubmit }: Props) {
};

return (
<div className="fixed left-1/2 bottom-0 -translate-x-1/2 w-full max-w-[64rem] px-3 py-4 bg-primary">
<form onSubmit={(e) => e.preventDefault()}>
<div className="flex items-end w-full gap-2">
<div className="fixed left-0 bottom-0 w-full px-3 py-4 flex-center bg-primary">
<form onSubmit={(e) => e.preventDefault()} className="w-full max-w-[64rem]">
<div className="flex items-end gap-2">
<label htmlFor="chatInput" className="sr-only">
질문 입력창
</label>
Expand Down
19 changes: 12 additions & 7 deletions src/domains/recommend/hook/useChatScroll.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';

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

// 새 메시지가 들어오면 자동 스크롤
useEffect(() => {
if (isScrollBottom.current) {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowNewMessageAlert(false); // 새메세지 숨김
} else {
setShowNewMessageAlert(true); // 새메세지 보여줌
if (!isScrollBottom.current) {
setShowNewMessageAlert(true);
return;
}
}, [messagesLength]);

const frameId = requestAnimationFrame(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'auto' });
setShowNewMessageAlert(false);
});

return () => cancelAnimationFrame(frameId);
}, [lastMessageId]);

// 스크롤 제일 아래로
const handleScrollToBottom = () => {
Expand Down
6 changes: 0 additions & 6 deletions src/domains/recommend/types/recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,5 @@ export interface ChatListProps {
stepData?: StepRecommendation | null
) => RecommendationItem[];

chatListRef: React.RefObject<HTMLDivElement | null>;
chatEndRef: React.RefObject<HTMLDivElement | null>;
showNewMessageAlert: boolean;
handleCheckBottom: (e: React.UIEvent<HTMLDivElement>) => void;
handleScrollToBottom: () => void;

isBotTyping: boolean;
}