Skip to content
Binary file not shown.
4 changes: 3 additions & 1 deletion frontend/src/apis/firebase/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -48,7 +50,7 @@ const getUserData = async () => {

return {
uid,
...docSnap.data(),
...userData,
likeShoes,
shoeCloset,
};
Expand Down
90 changes: 80 additions & 10 deletions frontend/src/apis/firebase/chatFirestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
setDoc,
FieldValue,
writeBatch,
startAfter,
QueryDocumentSnapshot,
DocumentData,
} from "firebase/firestore";
import { TChatResponse } from "@/types/chat";

Expand All @@ -26,6 +29,7 @@
content: string | TChatResponse;
id?: string;
}[];
lastVisibleDoc: QueryDocumentSnapshot<DocumentData> | null;
}> => {
const roomsCollection = collection(db, "chatSessions", userId, "rooms");
// 이 코드는 userId라는 도큐먼트의 하위 컬렉션 rooms에 접근하여,
Expand All @@ -48,7 +52,7 @@
});

latestRoomId = newRoomRef.id;
return { roomId: latestRoomId, messages: [] };
return { roomId: latestRoomId, messages: [], lastVisibleDoc: null };
} else {
const latestRoom = roomsSnapshot.docs[0];
latestRoomId = latestRoom.id;
Expand All @@ -62,12 +66,19 @@
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() !== "") {
Expand All @@ -89,7 +100,10 @@
return result;
});

return { roomId: latestRoomId, messages };
const lastVisibleDoc =
messagesSnapshot.docs[messagesSnapshot.docs.length - 1] || null;

return { roomId: latestRoomId, messages, lastVisibleDoc };
};

export const addMessageToFirestore = async (
Expand Down Expand Up @@ -132,9 +146,8 @@

// 성공적으로 메시지가 저장되었으므로 doc.id 반환
return docRef.id;
} catch (error) {

Check warning on line 149 in frontend/src/apis/firebase/chatFirestore.ts

View workflow job for this annotation

GitHub Actions / continuous-integration

'error' is defined but never used
console.error("메시지 추가 중 에러 발생: ", error);
return null; // 에러 발생 시 null 반환
throw new Error();
}
};

Expand Down Expand Up @@ -257,6 +270,7 @@
content: string | TChatResponse;
id?: string;
}[];
lastVisibleDoc: QueryDocumentSnapshot<DocumentData> | null;
}> => {
try {
const messagesCollection = collection(
Expand All @@ -269,11 +283,12 @@
);
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 = [];

Expand All @@ -295,10 +310,13 @@
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 };
}
};

Expand Down Expand Up @@ -330,3 +348,55 @@
console.error(error);
}
};

// 스크롤 위로 올렸을 때 호출할 함수
export const getOlderMessages = async (
userId: string,
roomId: string,
lastVisibleDoc: QueryDocumentSnapshot<DocumentData> | 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 };
};
12 changes: 12 additions & 0 deletions frontend/src/components/Chat/ChatLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const ChatLoading = () => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-800 bg-opacity-15">

Check warning on line 3 in frontend/src/components/Chat/ChatLoading.tsx

View workflow job for this annotation

GitHub Actions / continuous-integration

Classname 'bg-opacity-15' should be replaced by an opacity suffix (eg. '/15')
<div className="flex space-x-6">
<div className="h-3 w-3 animate-pulse rounded-full bg-gray-400" />
<div className="animation-delay-200 h-3 w-3 animate-pulse rounded-full bg-gray-500" />

Check warning on line 6 in frontend/src/components/Chat/ChatLoading.tsx

View workflow job for this annotation

GitHub Actions / continuous-integration

Classname 'animation-delay-200' is not a Tailwind CSS class!
<div className="animation-delay-400 h-3 w-3 animate-pulse rounded-full bg-gray-600" />

Check warning on line 7 in frontend/src/components/Chat/ChatLoading.tsx

View workflow job for this annotation

GitHub Actions / continuous-integration

Classname 'animation-delay-400' is not a Tailwind CSS class!
</div>
</div>
);
};
export default ChatLoading;
23 changes: 20 additions & 3 deletions frontend/src/components/Chat/ChatProductItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
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;
Expand All @@ -28,10 +30,18 @@
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) {
await addOrRemoveShoeFromLikes(user?.uid!, {

Check warning on line 44 in frontend/src/components/Chat/ChatProductItem.tsx

View workflow job for this annotation

GitHub Actions / continuous-integration

Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong
brand,
productName,
imgUrl,
Expand All @@ -41,8 +51,15 @@
});
updateUserInfo();
} else {
console.log("로그인이 필요합니다.");
// 여기서 로그인하라는 채팅을 띄워주면 좋을 듯 하다. 일단 나중에 ..
addGuestMessage({
type: "bot",
content: {
message: "로그인이 필요한 기능입니다.",
},
});
setTimeout(() => {
goToLogin();
}, 1500);
}
};
return (
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/components/Chat/ChatShareDislikeBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -44,7 +55,15 @@ const ChatShareDislikeBox = (props: ChatShareDislikeBoxProps) => {
console.error(error);
}
} else {
console.log("로그인 해야 모달창이 열린단다");
addGuestMessage({
type: "bot",
content: {
message: "로그인이 필요한 기능입니다.",
},
});
setTimeout(() => {
goToLogin();
}, 1500);
}
};

Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -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 (
<div className="flex flex-col items-center">
<div className="relative w-full min-w-80 max-w-5xl">
<Outlet />
<Outlet context={{ isAuthLoading }} />
</div>
</div>
);
Expand Down
Loading
Loading