diff --git a/frontend/.yarn/cache/react-webcam-npm-7.2.0-4404f3658d-d639a9e4cd.zip b/frontend/.yarn/cache/react-webcam-npm-7.2.0-4404f3658d-d639a9e4cd.zip new file mode 100644 index 0000000..a8a2b73 Binary files /dev/null and b/frontend/.yarn/cache/react-webcam-npm-7.2.0-4404f3658d-d639a9e4cd.zip differ diff --git a/frontend/src/apis/firebase/auth.ts b/frontend/src/apis/firebase/auth.ts index 0e24c4c..e583fb1 100644 --- a/frontend/src/apis/firebase/auth.ts +++ b/frontend/src/apis/firebase/auth.ts @@ -32,6 +32,8 @@ const getUserData = async () => { return null; } + const userData = docSnap.data() as TUser; + const likeShoesRef = collection(db, "users", uid, "likeShoes"); const likeShoesSnap = await getDocs(likeShoesRef); const likeShoes = likeShoesSnap.docs.map((doc) => ({ @@ -48,7 +50,7 @@ const getUserData = async () => { return { uid, - ...docSnap.data(), + ...userData, likeShoes, shoeCloset, }; diff --git a/frontend/src/apis/firebase/chatFirestore.ts b/frontend/src/apis/firebase/chatFirestore.ts index 1065b9b..80003da 100644 --- a/frontend/src/apis/firebase/chatFirestore.ts +++ b/frontend/src/apis/firebase/chatFirestore.ts @@ -14,6 +14,9 @@ import { setDoc, FieldValue, writeBatch, + startAfter, + QueryDocumentSnapshot, + DocumentData, } from "firebase/firestore"; import { TChatResponse } from "@/types/chat"; @@ -26,6 +29,7 @@ export const getMessagesFromLatestRoom = async ( content: string | TChatResponse; id?: string; }[]; + lastVisibleDoc: QueryDocumentSnapshot | null; }> => { const roomsCollection = collection(db, "chatSessions", userId, "rooms"); // 이 코드는 userId라는 도큐먼트의 하위 컬렉션 rooms에 접근하여, @@ -48,7 +52,7 @@ export const getMessagesFromLatestRoom = async ( }); latestRoomId = newRoomRef.id; - return { roomId: latestRoomId, messages: [] }; + return { roomId: latestRoomId, messages: [], lastVisibleDoc: null }; } else { const latestRoom = roomsSnapshot.docs[0]; latestRoomId = latestRoom.id; @@ -62,12 +66,19 @@ export const getMessagesFromLatestRoom = async ( latestRoomId, "messages" ); - const messagesQuery = query(messagesCollection, orderBy("timestamp", "asc")); + const messagesQuery = query( + messagesCollection, + orderBy("timestamp", "desc"), + limit(5) + ); const messagesSnapshot = await getDocs(messagesQuery); - const messages = messagesSnapshot.docs.flatMap((doc) => { + const messages = messagesSnapshot.docs.reverse().flatMap((doc) => { const data = doc.data(); const result = []; + // 질문과 답변을 한쌍으로 저장했기에, 하나의 doc에 대해서 + // bot과 user 메세지 객체를 분리하여 저장한다. + // 이후에 type이 bot인지 user인지에 따라 화면에 그려지는 위치가 정해진다. // user 메시지가 빈 문자열이 아닌 경우에만 추가 if (data.user && data.user.trim() !== "") { @@ -89,7 +100,10 @@ export const getMessagesFromLatestRoom = async ( return result; }); - return { roomId: latestRoomId, messages }; + const lastVisibleDoc = + messagesSnapshot.docs[messagesSnapshot.docs.length - 1] || null; + + return { roomId: latestRoomId, messages, lastVisibleDoc }; }; export const addMessageToFirestore = async ( @@ -133,8 +147,7 @@ export const addMessageToFirestore = async ( // 성공적으로 메시지가 저장되었으므로 doc.id 반환 return docRef.id; } catch (error) { - console.error("메시지 추가 중 에러 발생: ", error); - return null; // 에러 발생 시 null 반환 + throw new Error(); } }; @@ -257,6 +270,7 @@ export const getMessagesByUserIdAndRoomId = async ( content: string | TChatResponse; id?: string; }[]; + lastVisibleDoc: QueryDocumentSnapshot | null; }> => { try { const messagesCollection = collection( @@ -269,11 +283,12 @@ export const getMessagesByUserIdAndRoomId = async ( ); const messagesQuery = query( messagesCollection, - orderBy("timestamp", "asc") + orderBy("timestamp", "desc"), + limit(5) ); const messagesSnapshot = await getDocs(messagesQuery); - const messages = messagesSnapshot.docs.flatMap((doc) => { + const messages = messagesSnapshot.docs.reverse().flatMap((doc) => { const data = doc.data(); const result = []; @@ -295,10 +310,13 @@ export const getMessagesByUserIdAndRoomId = async ( return result; }); - return { roomId, messages }; + const lastVisibleDoc = + messagesSnapshot.docs[messagesSnapshot.docs.length - 1] || null; + + return { roomId, messages, lastVisibleDoc }; } catch (error) { console.error(error); - return { roomId, messages: [] }; + return { roomId, messages: [], lastVisibleDoc: null }; } }; @@ -330,3 +348,55 @@ export const deleteChatRoom = async (userId: string, roomId: string) => { console.error(error); } }; + +// 스크롤 위로 올렸을 때 호출할 함수 +export const getOlderMessages = async ( + userId: string, + roomId: string, + lastVisibleDoc: QueryDocumentSnapshot | null +) => { + const messagesCollection = collection( + db, + "chatSessions", + userId, + "rooms", + roomId, + "messages" + ); + + const messagesQuery = query( + messagesCollection, + orderBy("timestamp", "desc"), + startAfter(lastVisibleDoc), // 마지막 스냅샷 이후부터 가져옴 + limit(5) + ); + + const messagesSnapshot = await getDocs(messagesQuery); + + const messages = messagesSnapshot.docs.reverse().flatMap((doc) => { + const data = doc.data(); + const result = []; + + if (data.user && data.user.trim() !== "") { + result.push({ + type: "user" as const, + content: data.user as string, + }); + } + + if (data.bot) { + result.push({ + type: "bot" as const, + content: data.bot as TChatResponse, + id: doc.id, + }); + } + + return result; + }); + + const newLastVisibleDoc = + messagesSnapshot.docs[messagesSnapshot.docs.length - 1]; + + return { messages, newLastVisibleDoc }; +}; diff --git a/frontend/src/components/Chat/ChatLoading.tsx b/frontend/src/components/Chat/ChatLoading.tsx new file mode 100644 index 0000000..087b3f5 --- /dev/null +++ b/frontend/src/components/Chat/ChatLoading.tsx @@ -0,0 +1,12 @@ +const ChatLoading = () => { + return ( +
+
+
+
+
+
+
+ ); +}; +export default ChatLoading; diff --git a/frontend/src/components/Chat/ChatProductItem.tsx b/frontend/src/components/Chat/ChatProductItem.tsx index a5db168..5eaa944 100644 --- a/frontend/src/components/Chat/ChatProductItem.tsx +++ b/frontend/src/components/Chat/ChatProductItem.tsx @@ -2,7 +2,9 @@ import { addOrRemoveShoeFromLikes } from "@apis/firebase/likeFirestore"; import userStore from "@store/auth.store"; import LikeButton from "@common/LikeButton"; import Img from "@common/html/Img"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; +import useChatStore from "@/store/chat.store"; +import { useBottomSheet } from "@/store/bottomSheet.store"; interface ProductItemProps { brand: string; @@ -28,6 +30,14 @@ const ChatProductItem = (props: ProductItemProps) => { const { messageId } = useParams(); const isSharePage = Boolean(messageId); + const { addGuestMessage } = useChatStore(); + const { open } = useBottomSheet(); + const navigate = useNavigate(); + const goToLogin = () => { + navigate("#login"); + open("login"); + }; + const handleLikeClick = async (e: React.MouseEvent) => { e.stopPropagation(); if (isLoggedIn) { @@ -41,8 +51,15 @@ const ChatProductItem = (props: ProductItemProps) => { }); updateUserInfo(); } else { - console.log("로그인이 필요합니다."); - // 여기서 로그인하라는 채팅을 띄워주면 좋을 듯 하다. 일단 나중에 .. + addGuestMessage({ + type: "bot", + content: { + message: "로그인이 필요한 기능입니다.", + }, + }); + setTimeout(() => { + goToLogin(); + }, 1500); } }; return ( diff --git a/frontend/src/components/Chat/ChatShareDislikeBox.tsx b/frontend/src/components/Chat/ChatShareDislikeBox.tsx index 1d2405e..16a0ab2 100644 --- a/frontend/src/components/Chat/ChatShareDislikeBox.tsx +++ b/frontend/src/components/Chat/ChatShareDislikeBox.tsx @@ -10,6 +10,8 @@ import { TChatResponse } from "@/types/chat"; import { Timestamp } from "firebase/firestore"; import ShareModal from "@common/ShareModal"; import useToggle from "@hooks/useToggle"; +import { useNavigate } from "react-router-dom"; +import { useBottomSheet } from "@/store/bottomSheet.store"; interface ChatShareDislikeBoxProps { docId?: string | null; @@ -29,9 +31,18 @@ const ChatShareDislikeBox = (props: ChatShareDislikeBoxProps) => { timestamp: Timestamp; }>(); const { user } = userStore(); - const { roomId } = useChatStore(); + const { roomId, addGuestMessage } = useChatStore(); const userId = user?.uid!; + const { open } = useBottomSheet(); + + const navigate = useNavigate(); + + const goToLogin = () => { + navigate("#login"); + open("login"); + }; + const handleOpenModal = async () => { if (user) { try { @@ -44,7 +55,15 @@ const ChatShareDislikeBox = (props: ChatShareDislikeBoxProps) => { console.error(error); } } else { - console.log("로그인 해야 모달창이 열린단다"); + addGuestMessage({ + type: "bot", + content: { + message: "로그인이 필요한 기능입니다.", + }, + }); + setTimeout(() => { + goToLogin(); + }, 1500); } }; diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx index f0244e2..9bd62aa 100644 --- a/frontend/src/components/layout/Layout.tsx +++ b/frontend/src/components/layout/Layout.tsx @@ -1,7 +1,7 @@ import { auth } from "@/firebase"; import userStore from "@/store/auth.store"; import { onAuthStateChanged } from "firebase/auth"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Outlet } from "react-router-dom"; const Layout = () => { @@ -10,23 +10,32 @@ const Layout = () => { setIsLoggedIn: store.setIsLoggedIn, })); + const [isAuthLoading, setIsAuthLoading] = useState(true); + + // onAuthStateChanged는 Firebase Auth가 제공하는 상태 변화 감지 메서드 + // 사용자가 로그인하거나 로그아웃할 때 이벤트를 트리거 + // user 객체는 로그인된 사용자의 정보이며, 로그아웃 상태일 때는 null + // unmount시 Firebase Auth의 상태 변화 구독을 중지 + // 이를 통해 메모리 누수와 불필요한 상태 업데이트를 방지 useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (user) => { if (user) { setIsLoggedIn(true); updateUserInfo(); } else { + // 로그인 된 상태면 여기 안탄다.(깜빡임도 존재하지 않음) setIsLoggedIn(false); } + setIsAuthLoading(false); }); return () => unsubscribe(); - }, [setIsLoggedIn, updateUserInfo]); + }, []); return (
- +
); diff --git a/frontend/src/components/login/Login.tsx b/frontend/src/components/login/Login.tsx index c4d0fd5..72cc280 100644 --- a/frontend/src/components/login/Login.tsx +++ b/frontend/src/components/login/Login.tsx @@ -14,7 +14,7 @@ import useFocus from "@hooks/useFocus"; const Login = () => { const navigate = useNavigate(); - const { updateUserInfo, user, isLoggedIn } = userStore(); + const { updateUserInfo, isLoggedIn } = userStore(); const { roomId, addUserMessage } = useChatStore(); const [loginData, setLoginData] = useState({ email: "", @@ -23,7 +23,13 @@ const Login = () => { const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); - const { closeAll } = useBottomSheet(); + const { closeAll, open } = useBottomSheet(); + + const goToSignUp = () => { + closeAll(); + navigate("#signup"); + open("login"); + }; // 자동 포커스 const [emailRef, focusEmail, handleEmailKeyPress] = @@ -81,24 +87,25 @@ const Login = () => { //값 확인용 if (isLoginValid) { - await signInWithCredential(loginData).then(() => { - updateUserInfo(); - closeAll(); - navigate("/"); - }); - - // 여기서 왜 isLoggedIn이 false 일까.. - // 루트 경로로 보냈으니 Layout 컴포넌트에서 로그인 상태 변경 해줘야 하는거 아닌가?? - // 일단 급한대로 아래 try문에서 로그인 상태 확인 없이 진행 - console.log(isLoggedIn); + await signInWithCredential(loginData); + // zustand로 관리하는 user가 업데이트가 바로 안이루어져서, + // 임시 방편으로 updateUserInfo 가 userData를 반환하게끔 하고 + // 반환값을 사용하도록 하자 + // 필요한 데이터만 구조분해할당 + const { uid, username } = (await updateUserInfo()) as { + uid: string; + username: string; + }; + closeAll(); + navigate("/"); // 여기서 맞춤상품 api 호출 처리 try { - const loginMent = `반갑습니다 ${user?.username}님! ${user?.username}님을 위한 맞춤 상품을 추천해 드릴께요`; + const loginMent = `반갑습니다 ${username!}님! ${username!}님을 위한 맞춤 상품을 추천해 드릴께요`; const res = await chatApi.getCustomizedProduct(); if (res.status === 200) { - await addMessageToFirestore(user?.uid!, roomId!, "", { + await addMessageToFirestore(uid!, roomId!, "", { message: loginMent, reqProducts: res.data, } as TChatResponse); @@ -152,6 +159,12 @@ const Login = () => { onKeyDown={(e) => handlePasswordPress(e, focusFormButton)} /> + + 아이디가 없으신가요? +
{/*로그인 폼 제출 버튼*/} diff --git a/frontend/src/components/sidemenu/SideMenu.tsx b/frontend/src/components/sidemenu/SideMenu.tsx index 385fbad..f536ef5 100644 --- a/frontend/src/components/sidemenu/SideMenu.tsx +++ b/frontend/src/components/sidemenu/SideMenu.tsx @@ -119,7 +119,10 @@ const SideMenu = ({ await deleteChatRoom(userId, deleteId); const updatedChats = await getUserChatRooms(user?.uid!); setChats(updatedChats); - // 지우려는 방이 현재 속한 방이라면 채팅방의 내용을 가장 최신 채팅으로 업데이트 + + // 지우려는 방이 현재 속한 방이라면 채팅방의 내용을 + // 가장 최신 채팅이 이루어진 채팅방으로 업데이트 + // (채팅방을 삭제했을 때 채팅창에 메세지가 전부 다 사라지면 사용자 경험 측면에서 안좋으니.) if (deleteId === roomId) { loadUserMessages(userId); } diff --git a/frontend/src/components/signup/SignUpRequired.tsx b/frontend/src/components/signup/SignUpRequired.tsx index f4abe5a..5a40306 100644 --- a/frontend/src/components/signup/SignUpRequired.tsx +++ b/frontend/src/components/signup/SignUpRequired.tsx @@ -8,12 +8,15 @@ import { useInput } from "@hooks/useInput"; import userStore from "@store/auth.store"; import { auth } from "@/firebase"; import useFocus from "@hooks/useFocus"; +import { useBottomSheet } from "@/store/bottomSheet.store"; +import { useNavigate } from "react-router-dom"; const SignUpRequired = () => { const user = auth.currentUser; const { updateUserInfo } = userStore((store) => ({ updateUserInfo: store.updateUserInfo, })); + const { closeAll, open } = useBottomSheet(); const { value: signUpRequired, setValue: setSignUpRequired } = useInput({ email: user?.email || "", password: user ? "blocked" : "", @@ -178,6 +181,14 @@ const SignUpRequired = () => { } }; + const navigate = useNavigate(); + + const goToLogin = () => { + closeAll(); + navigate("#login"); + open("login"); + }; + return ( <>
@@ -276,6 +287,12 @@ const SignUpRequired = () => { />
+ + 이미 아이디가 있으신가요? +
{/*회원가입 다음 페이지로 이동 버튼*/} diff --git a/frontend/src/pages/Chat/Chat.tsx b/frontend/src/pages/Chat/Chat.tsx index 330e800..f93cebd 100644 --- a/frontend/src/pages/Chat/Chat.tsx +++ b/frontend/src/pages/Chat/Chat.tsx @@ -1,10 +1,10 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { motion } from "framer-motion"; import ChatInput from "@components/Chat/ChatInput"; import ChatMessage from "@components/Chat/ChatMessage"; import ChatUserMessage from "@components/Chat/ChatUserMessage"; import Header from "@common/Header"; -import { TChatResponse } from "@type/chat"; +import { OutletContextType, TChatResponse } from "@type/chat"; import ChatLogin from "@components/Chat/ChatLogin"; import ChatRecommendedQuestion from "@components/Chat/ChatRecommendedQuestion"; import LoginBottomSheet from "@components/login/LoginBottomSheet"; @@ -15,10 +15,18 @@ import productAndBrandStore from "@store/productAndBrand.store"; import InterestKeywordsBottomSheet from "@components/Chat/InterestKeywordsBottomSheet"; import { useBottomSheet } from "@store/bottomSheet.store"; import ChatSampleQuestions from "@components/Chat/ChatSampleQuestions"; +import { useOutletContext } from "react-router-dom"; +import ChatLoading from "@/components/Chat/ChatLoading"; const Chat = () => { - const { guestMessages, userMessages, loadGuestMessages, loadUserMessages } = - useChatStore(); + const { + guestMessages, + userMessages, + roomId, + loadGuestMessages, + loadUserMessages, + loadOlderMessages, + } = useChatStore(); const { clickedProducts, clickedBrand, setClickedProducts, setClickedBrand } = productAndBrandStore(); @@ -75,11 +83,38 @@ const Chat = () => { }, [isLoggedIn, userId]); const mainRef = useRef(null); + const [isLoadingOlderMessages, setIsLoadingOlderMessages] = useState(false); + + const handleScroll = () => { + if (mainRef.current) { + const { scrollTop } = mainRef.current; + if (scrollTop === 0) { + setIsLoadingOlderMessages(true); + setTimeout(() => { + loadOlderMessages(userId!, roomId!).finally(() => { + setIsLoadingOlderMessages(false); + }); + }, 1000); + } + } + }; + + // roomId가 비동기적으로(?) update 돼서 roomId가 제대로 들어왔을 때만 동작하게끔 + useEffect(() => { + const mainElement = mainRef.current; + if (mainElement && roomId) { + mainElement.addEventListener("scroll", handleScroll); + + return () => { + mainElement.removeEventListener("scroll", handleScroll); + }; + } + }, [roomId]); // 스크롤을 항상 하단에 위치시키기 useLayoutEffect(() => { // 바텀 오픈 X 일 경우 - if (!isAllSheetsOpen) { + if (!isAllSheetsOpen && !isLoadingOlderMessages) { setTimeout(() => { mainRef.current?.scrollTo({ top: mainRef.current.scrollHeight, @@ -87,10 +122,11 @@ const Chat = () => { }); }, 0); } - }, [ - isLoggedIn ? userMessages.length : guestMessages.length, - isAllSheetsOpen, - ]); + // isAllSheetsOpen 를 의존성 배열에 넣으면, 바텀시트 닫을 때 스크롤이 최하단으로 움직여서 일단 뺌 + }, [userMessages, guestMessages]); + + // 로그인 된 유저가 새로고침할 때 생기는 깜빡임을 제어할 state + const { isAuthLoading } = useOutletContext(); return (
@@ -100,22 +136,30 @@ const Chat = () => { {/* 현재 질문 가능한 목록 */} + {/* 이전 메세지를 보이는 UI */} + {isLoadingOlderMessages && } +
- {!isLoggedIn && } - - {(isLoggedIn ? userMessages : guestMessages).map((msg, index) => ( -
- {msg.type === "user" ? ( - - ) : ( - - )} -
- ))} - {/*
*/} + {isAuthLoading ? ( + + ) : ( + <> + {!isLoggedIn && } + + {(isLoggedIn ? userMessages : guestMessages).map((msg, index) => ( +
+ {msg.type === "user" ? ( + + ) : ( + + )} +
+ ))} + + )}
diff --git a/frontend/src/store/auth.store.ts b/frontend/src/store/auth.store.ts index 506a75a..0dba059 100644 --- a/frontend/src/store/auth.store.ts +++ b/frontend/src/store/auth.store.ts @@ -39,7 +39,7 @@ interface UserState { likeShoes: likeShoes; // 신발장의 신발들 관리하는 상태 shoeCloset: shoeCloset; - updateUserInfo: () => void; + updateUserInfo: () => Promise; // async 함수는 암묵적으로 Promise를 반환한다. setUserInfo: (key: string, value: string | number) => void; setIsLoggedIn: (state: boolean) => void; } @@ -49,8 +49,9 @@ const userStore = create((set) => ({ user: null, likeShoes: null, shoeCloset: null, - updateUserInfo: () => { - getUserData().then((data) => { + updateUserInfo: async () => { + try { + const data = await getUserData(); if (data) { const { likeShoes, shoeCloset, ...userData } = data; set({ @@ -58,10 +59,13 @@ const userStore = create((set) => ({ likeShoes: likeShoes as likeShoes, shoeCloset: shoeCloset as shoeCloset, }); + return userData; } else { set({ user: null }); } - }); + } catch (error) { + console.error(error); + } }, setUserInfo: (key: string, value: string | number) => { set((user) => ({ ...user, [key]: value })); diff --git a/frontend/src/store/chat.store.ts b/frontend/src/store/chat.store.ts index 815b1a8..8c12914 100644 --- a/frontend/src/store/chat.store.ts +++ b/frontend/src/store/chat.store.ts @@ -3,7 +3,9 @@ import { TChatResponse } from "@/types/chat"; import { getMessagesByUserIdAndRoomId, getMessagesFromLatestRoom, + getOlderMessages, } from "@/apis/firebase/chatFirestore"; +import { DocumentData, QueryDocumentSnapshot } from "firebase/firestore"; interface UserMessage { type: "user"; @@ -20,19 +22,22 @@ interface ChatState { guestMessages: (UserMessage | BotMessage)[]; userMessages: (UserMessage | BotMessage)[]; roomId: string | null; + lastVisibleDoc: QueryDocumentSnapshot | null; setRoomId: (roomId: string) => void; addGuestMessage: (message: UserMessage | BotMessage) => void; addUserMessage: (message: UserMessage | BotMessage) => void; loadGuestMessages: () => void; - loadUserMessages: (userId: string) => void; + loadUserMessages: (userId: string) => Promise; getMessagesByRoomId: (userId: string, roomId: string) => void; + loadOlderMessages: (userId: string, roomId: string) => Promise; clearGuestMessages: () => void; } -const useChatStore = create((set) => ({ +const useChatStore = create((set, get) => ({ guestMessages: [], userMessages: [], roomId: null, + lastVisibleDoc: null, // roomId 설정 함수 setRoomId: (roomId: string) => set({ roomId }), @@ -62,7 +67,8 @@ const useChatStore = create((set) => ({ // 회원 메시지 로드 (Firestore에서) loadUserMessages: async (userId: string) => { - const { roomId, messages } = await getMessagesFromLatestRoom(userId); + const { roomId, messages, lastVisibleDoc } = + await getMessagesFromLatestRoom(userId); // 어디서부터 꼬인건지.. 일단 여기서 이렇게 해줘야 에러가 안나긴 한다. 나중에 찾아보자.. const formattedMessages = messages.map((msg) => { @@ -79,14 +85,16 @@ const useChatStore = create((set) => ({ } as UserMessage; } }); - - set({ roomId, userMessages: formattedMessages }); + set({ roomId, userMessages: formattedMessages, lastVisibleDoc }); }, // userId와 roomId를 인자로 받아 그에 해당하는 메세지를 가져오는 함수 getMessagesByRoomId: async (userId: string, roomId: string) => { - const { roomId: fetchedRoomId, messages } = - await getMessagesByUserIdAndRoomId(userId, roomId); + const { + roomId: fetchedRoomId, + messages, + lastVisibleDoc, + } = await getMessagesByUserIdAndRoomId(userId, roomId); // 어디서부터 꼬인건지.. 일단 여기서 이렇게 해줘야 에러가 안나긴 한다. 나중에 찾아보자.. const formattedMessages = messages.map((msg) => { @@ -103,10 +111,41 @@ const useChatStore = create((set) => ({ } as UserMessage; } }); - console.log(formattedMessages); - console.log(fetchedRoomId); - set({ roomId: fetchedRoomId, userMessages: formattedMessages }); + set({ + roomId: fetchedRoomId, + userMessages: formattedMessages, + lastVisibleDoc, + }); + }, + + loadOlderMessages: async (userId: string, roomId: string) => { + const { lastVisibleDoc } = get(); + if (!lastVisibleDoc) return; // 마지막 메시지가 없으면 더 이상 로드하지 않음 + const { messages, newLastVisibleDoc } = await getOlderMessages( + userId, + roomId, + lastVisibleDoc + ); + + const formattedMessages = messages.map((msg) => { + if (msg.type === "bot") { + return { + type: "bot", + content: msg.content, + id: msg.id, + } as BotMessage; + } else { + return { + type: "user", + content: msg.content, + } as UserMessage; + } + }); + set((state) => ({ + userMessages: [...formattedMessages, ...state.userMessages], + lastVisibleDoc: newLastVisibleDoc, + })); }, // 비회원 메시지 비우기 (로그인 시) diff --git a/frontend/src/types/chat.d.ts b/frontend/src/types/chat.d.ts index 2823cf1..2f9d3f4 100644 --- a/frontend/src/types/chat.d.ts +++ b/frontend/src/types/chat.d.ts @@ -28,3 +28,7 @@ export type TChatResponse = { productId: string; }[]; }; + +export type OutletContextType = { + isAuthLoading: boolean; +};