diff --git a/bookduck/package.json b/bookduck/package.json index 00bf08d3..8806b8e6 100644 --- a/bookduck/package.json +++ b/bookduck/package.json @@ -14,7 +14,7 @@ "@react-spring/web": "^9.7.5", "@react-three/drei": "^9.115.0", "@react-three/fiber": "^8.17.10", - "@tanstack/react-query": "^5.60.2", + "@tanstack/react-query": "^5.61.3", "axios": "^1.7.7", "classnames": "^2.5.1", "html2canvas": "^1.4.1", diff --git a/bookduck/src/App.jsx b/bookduck/src/App.jsx index 77e70070..9022220f 100644 --- a/bookduck/src/App.jsx +++ b/bookduck/src/App.jsx @@ -33,17 +33,29 @@ import SettingPage from "./pages/SettingPage/SettingPage"; import FriendListPage from "./pages/FriendPage/FriendListPage"; import OAuthRedierctPage from "./pages/LoginPage/OAuthRedierctPage"; import OtherMainPage from "./pages/OtherUserPage/OtherMainPage"; +import handleFcmToken from "./components/NotificationPage/handleFcmToken"; +import { getUserId } from "./api/oauth"; function App() { useEffect(() => { - const fetchFcmToken = async () => { - const token = await requestFcmToken(); - if (token) { - console.log("FCM 토큰을 서버로 전송하거나 저장:", token); + const fetchandSendFCM = async () => { + try { + const id = await getUserId(); + const accessToken = localStorage.getItem("token"); + + if (!id || !accessToken) { + console.error("사용자 ID 또는 액세스 토큰이 없습니다."); + return; + } + await handleFcmToken(id); + } catch (error) { + console.error("FCM 처리 중 오류 발생:", error.message); } }; - fetchFcmToken(); + + fetchandSendFCM(); }, []); + useEffect(() => { // 포그라운드 메시지 수신 처리 onMessage(messaging, (payload) => { diff --git a/bookduck/src/api/fcm.js b/bookduck/src/api/fcm.js index 491d2cf4..19958871 100644 --- a/bookduck/src/api/fcm.js +++ b/bookduck/src/api/fcm.js @@ -18,7 +18,6 @@ export const requestFcmToken = async () => { }); if (fcmToken) { - console.log("FCM 토큰:", fcmToken); return fcmToken; } else { console.error("FCM 토큰을 가져오지 못했습니다."); diff --git a/bookduck/src/api/fcmApi.js b/bookduck/src/api/fcmApi.js index fc68adea..f26ae445 100644 --- a/bookduck/src/api/fcmApi.js +++ b/bookduck/src/api/fcmApi.js @@ -1,12 +1,14 @@ import { post } from "./example"; -export const sendFcmToken = async (userId, fcmToken) => { +import { postAccessTokenIssue } from "./oauth"; +export const postFcmToken = async (userId, fcmToken) => { try { // POST 요청으로 FCM 토큰 전송 - const response = await post(`/fcm/${userId}/token`, { fcmToken }); + const response = await post(`/fcm/${userId}/token`, { + fcmToken: fcmToken, + }); console.log("FCM 토큰 전송 성공:", response); return response; } catch (error) { console.error("FCM 토큰 전송 실패:", error); - throw error; } }; diff --git a/bookduck/src/assets/otherUserPage/readingspace-duck.svg b/bookduck/src/assets/otherUserPage/readingspace-duck.svg new file mode 100644 index 00000000..aa7055a7 --- /dev/null +++ b/bookduck/src/assets/otherUserPage/readingspace-duck.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/bookduck/src/components/MainPage/ReadingSpaceComponent.jsx b/bookduck/src/components/MainPage/ReadingSpaceComponent.jsx index 1ee6ff41..d5276af4 100644 --- a/bookduck/src/components/MainPage/ReadingSpaceComponent.jsx +++ b/bookduck/src/components/MainPage/ReadingSpaceComponent.jsx @@ -11,13 +11,13 @@ import goEdit from "../../assets/mainPage/go-edit.svg"; import goRight from "../../assets/mainPage/go-right.svg"; import cancel from "../../assets/mainPage/cancel.svg"; import menu from "../../assets/mainPage/menu-vertical.svg"; -import recordCircleIcon from "../../assets/recordingPage/record-circle-icon.svg"; import editIcon from "../../assets/bookinfoPage/edit.svg"; import deleteIcon from "../../assets/bookinfoPage/trash.svg"; import plusIcon from "../../assets/mainPage/plus.svg"; import helpCircle from "../../assets/mainPage/help-circle.svg"; import DraggableList from "./DraggableList"; import { getUserId } from "../../api/oauth"; +import ReadingSpaceDuck from "../../assets/otherUserPage/readingspace-duck.svg"; const ReadingSpaceComponent = ({ setColor, @@ -56,9 +56,8 @@ const ReadingSpaceComponent = ({ try { const id = isMine ? await getUserId() : otherUserId; setUserId(id); - const response = await get(`/users/${id}/readingspace`); - console.log(response); + // console.log(response); setCards(response.cardList); } catch (error) { console.error("리딩스페이스 조회 오류", error); @@ -101,14 +100,6 @@ const ReadingSpaceComponent = ({ getCards(); }, []); - useEffect(() => { - console.log(cards); - }, [cards]); - - useEffect(() => { - console.log("otehruseif", otherUserId); - }, [otherUserId]); - useEffect(() => { if (isAllDelete) { patchCards([]).then(() => { @@ -135,8 +126,10 @@ const ReadingSpaceComponent = ({ const handleMenuClick = () => { if (height.get() < expandedHeight) { api.start({ height: expandedHeight }); + setBottomSheetShow(true); + } else { + setBottomSheetShow(true); } - setBottomSheetShow(true); }; const handleEditClick = () => { @@ -287,7 +280,12 @@ const ReadingSpaceComponent = ({
{isMine && ( - menu +
+ menu +
)}
@@ -305,22 +303,31 @@ const ReadingSpaceComponent = ({ className="flex flex-col gap-1" > {cards.length === 0 ? ( - // 카드가 없을 때 렌더링 -
-
- 리딩 스페이스가 텅 비어있네요! + isMine ? ( + // 카드가 없을 때 렌더링 +
+
+ 리딩 스페이스가 텅 비어있네요! +
+
+ 나만의 리딩 스페이스를 꾸며보세요 +
+ navigate("selectcard")} + />
-
- 나만의 리딩 스페이스를 꾸며보세요 + ) : ( +
+

+ 리딩스페이스가 텅 비어있어요. +

+
- navigate("selectcard")} - /> -
+ ) ) : ( // 카드가 있을 때 렌더링 )} - {isFloatingVisible && ( + {/* {isFloatingVisible && (
navigate("/selectbook")} @@ -360,7 +367,7 @@ const ReadingSpaceComponent = ({ alt="record_circle_icon" />
- )} + )} */}
diff --git a/bookduck/src/components/NotificationPage/GeneralNotiComponent.jsx b/bookduck/src/components/NotificationPage/GeneralNotiComponent.jsx index f8832eaa..7b94ce4c 100644 --- a/bookduck/src/components/NotificationPage/GeneralNotiComponent.jsx +++ b/bookduck/src/components/NotificationPage/GeneralNotiComponent.jsx @@ -1,123 +1,101 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useRef } from "react"; +import { + useInfiniteQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; import { get, patch } from "../../api/example"; import NotificationItemComponent from "./NotificationItemComponent"; import { useSSE } from "../../context/SSEProvider"; -/*API-일반 알람 읽음 처리*/ + +/* API - 일반 알람 읽음 처리 */ export const patchAlarm = async (alarmId) => { - try { - await patch(`/alarms/common`, { - alarmId: alarmId, - }); - console.log("일반 알림 읽음 처리 완료"); - } catch (error) { - console.error("일반 알람 읽음 처리 에러", error); - } + await patch(`/alarms/common`, { alarmId }); }; -const GeneralNotiComponent = () => { - const [notifications, setNotifications] = useState([]); - const [currentPage, setCurrentPage] = useState(0); - const [totalPages, setTotalPages] = useState(1); - const [isLoading, setIsLoading] = useState(false); - const loaderRef = useRef(null); - const DATA_LIMIT = 10; - const { sseData } = useSSE(); +/* API - 알람 리스트 받기 */ +export const fetchAlarmList = async ({ pageParam = 0 }) => { + const DATA_LIMIT = 10; + const response = await get( + `/alarms/common?page=${pageParam}&size=${DATA_LIMIT}` + ); + const { pageContent, totalPages } = response; - /* API-알람 리스트 받기*/ - const getAlarmList = async (page = 0) => { - try { - const response = await get( - `/alarms/common?page=${page}&size=${DATA_LIMIT}` - ); - console.log("response", response); - const data = response.pageContent.map((alarm) => ({ - resourceId: alarm.resourceId, - alarmId: alarm.alarmId, - isRead: alarm.isRead, - alarmType: alarm.alarmType, - createdTime: alarm.createdTime, - boldText: alarm.boldText, - })); - setNotifications((n) => [...n, ...data]); - setTotalPages(response.totalPages || 1); - } catch (error) { - console.error("알람 읽기 오류", error); - } + return { + pageContent, + nextPage: pageParam + 1, + totalPages: totalPages || 1, }; +}; + +const GeneralNotiComponent = () => { + const loaderRef = useRef(null); + const queryClient = useQueryClient(); + const { sseData } = useSSE(); - // 초기화 로직 - useEffect(() => { - const initialize = async () => { - setNotifications([]); // 기존 데이터 초기화 - setCurrentPage(0); // 첫 페이지로 초기화 - setIsLoading(true); - await getAlarmList(0); - setIsLoading(false); - }; + // React Query - 알람 리스트 가져오기 + const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = + useInfiniteQuery({ + queryKey: ["alarmList"], + queryFn: fetchAlarmList, + getNextPageParam: (lastPage) => + lastPage.nextPage < lastPage.totalPages ? lastPage.nextPage : undefined, + }); - initialize(); - }, []); + // React Query - 알람 읽음 처리 + const { mutate: markAsRead } = useMutation({ + mutationFn: patchAlarm, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["alarmList"] }); + }, + }); // SSE 데이터 처리 - useEffect(() => { + React.useEffect(() => { if (!sseData.isCommonAlarmChecked) { - console.log("새로운 일반 알람 확인"); - const newAlarm = { - alarmId: sseData.alarmId, - isRead: sseData.isRead, - alarmType: sseData.alarmType, - createdTime: sseData.createdTime, - boldText: sseData.boldText, - }; - setNotifications((prev) => [newAlarm, ...prev]); + queryClient.invalidateQueries({ queryKey: ["alarmList"] }); } - }, [sseData]); - - // 무한 스크롤 감지 - useEffect(() => { - const handleObserver = (entries) => { - const [entry] = entries; - if (entry.isIntersecting && currentPage < totalPages && !isLoading) { - console.log("다음 페이지 로드"); - setCurrentPage((p) => p + 1); - } - }; + }, [sseData, queryClient]); - const observer = new IntersectionObserver(handleObserver, { - root: null, - rootMargin: "0px", - threshold: 1.0, - }); + // Intersection Observer - 무한 스크롤 + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { root: null, rootMargin: "0px", threshold: 1.0 } + ); if (loaderRef.current) observer.observe(loaderRef.current); return () => observer.disconnect(); - }, [currentPage, totalPages, isLoading]); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); - // 현재 페이지 데이터 로드 - useEffect(() => { - if (currentPage > 0 && !isLoading) { - console.log(`페이지 ${currentPage} 데이터 로드`); - setIsLoading(true); - getAlarmList(currentPage).finally(() => setIsLoading(false)); - } - }, [currentPage]); + if (isLoading) { + return
로딩 중...
; + } return ( <> - {notifications.map((notification, index) => ( -
- -
- ))} -
+ {data.pages.map((page, pageIndex) => + page.pageContent.map((notification, index) => ( +
+ markAsRead(notification.alarmId)} + /> +
+ )) + )} +
{isFetchingNextPage &&
로딩 중...
}
); }; diff --git a/bookduck/src/components/NotificationPage/NotificationItemComponent.jsx b/bookduck/src/components/NotificationPage/NotificationItemComponent.jsx index cbf5d570..9e264592 100644 --- a/bookduck/src/components/NotificationPage/NotificationItemComponent.jsx +++ b/bookduck/src/components/NotificationPage/NotificationItemComponent.jsx @@ -99,8 +99,6 @@ const NotificationItemComponent = ({ }) => { const navigate = useNavigate(); const notificationData = notificationTemplates[alarmType]; - console.log(notificationData); - useEffect(() => { console.log("resourceId", resourceId); }, [resourceId]); diff --git a/bookduck/src/components/NotificationPage/handleFcmToken.js b/bookduck/src/components/NotificationPage/handleFcmToken.js index f2655e91..f572747d 100644 --- a/bookduck/src/components/NotificationPage/handleFcmToken.js +++ b/bookduck/src/components/NotificationPage/handleFcmToken.js @@ -1,19 +1,24 @@ import { requestFcmToken } from "../../api/fcm"; -import { sendFcmToken } from "../../api/fcmApi"; -const handleFcmToken = async (userId) => { - try { - // 1. FCM 토큰 가져오기 - const fcmToken = await requestFcmToken(); +import { postFcmToken } from "../../api/fcmApi"; - if (fcmToken) { - // 2. 서버로 FCM 토큰 전송 - await sendFcmToken(userId, fcmToken); - console.log("FCM 토큰 서버로 전송 완료"); - } else { - console.error("FCM 토큰 가져오기 실패"); +const handleFcmToken = async (userId) => { + const isTokenSent = JSON.parse(localStorage.getItem("isFcmTokenSent")); + if (!isTokenSent && userId) { + try { + // 1. FCM 토큰 가져오기 + const fcmToken = await requestFcmToken(); + if (fcmToken) { + // 2. 서버로 FCM 토큰 전송 + await postFcmToken(userId, fcmToken); + localStorage.setItem("isFcmTokenSent", JSON.stringify(true)); + } else { + console.error("FCM 토큰 가져오기 실패"); + } + } catch (error) { + console.error("FCM 처리 중 오류 발생:", error); } - } catch (error) { - console.error("FCM 처리 중 오류 발생:", error); + } else { + // console.log("FCM토큰이 이미 저장되었습니다."); } }; diff --git a/bookduck/src/components/OtherUserPage/Header.jsx b/bookduck/src/components/OtherUserPage/Header.jsx index 9bdad60c..557f0786 100644 --- a/bookduck/src/components/OtherUserPage/Header.jsx +++ b/bookduck/src/components/OtherUserPage/Header.jsx @@ -1,16 +1,13 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import backIcon from "../../assets/otherUserPage/back.svg"; -const Header = ({ name, isFriend = true, handleAddClick }) => { +const Header = ({ isFriend = true, handleAddClick }) => { const navigate = useNavigate(); return (
-
- - {name}의 홈 -
+ {isFriend ? ( main duck { const [tab, setTab] = useState("일반"); const { sseData } = useSSE(); - const [dotStates, setDotStates] = useState([ - !sseData.isCommonAlarmChecked, - !sseData.isAnnouncementChecked, - ]); + const [dotStates, setDotStates] = useState([false, false]); useEffect(() => { - const newDotStates = [ - !sseData.isCommonAlarmChecked, - !sseData.isAnnouncementChecked, - ]; - console.log("업데이트된 dotStates:", newDotStates); - setDotStates(newDotStates); + if (sseData) { + const newDotStates = [ + sseData.isCommonAlarmChecked === null + ? null + : !sseData.isCommonAlarmChecked, + sseData.isAnnouncementChecked === null + ? null + : !sseData.isAnnouncementChecked, + ]; + console.log("업데이트된 dotStates:", newDotStates); + setDotStates(newDotStates); + } }, [sseData]); return ( diff --git a/bookduck/src/pages/OtherUserPage/OtherMainPage.jsx b/bookduck/src/pages/OtherUserPage/OtherMainPage.jsx index 2d61d8a3..d0af928a 100644 --- a/bookduck/src/pages/OtherUserPage/OtherMainPage.jsx +++ b/bookduck/src/pages/OtherUserPage/OtherMainPage.jsx @@ -2,10 +2,15 @@ import React, { useState, useEffect } from "react"; import Header from "../../components/OtherUserPage/Header"; import StatusBar from "../../components/common/StatusBar"; import { get, post } from "../../api/example"; +import { useNavigate } from "react-router-dom"; import ReadingSpaceComponent from "../../components/MainPage/ReadingSpaceComponent"; +import BookCountDisplay from "../../components/MainPage/BookCountDisplay"; +import right from "../../assets/common/right-yellow.svg"; +import mainDuck from "../../assets/common/main-duck.svg"; import { useParams } from "react-router-dom"; const OtherMainPage = () => { + const navigate = useNavigate(); let { id: userId } = useParams(); const [userInfo, setUserInfo] = useState([]); //API 연결-정보받기 @@ -50,14 +55,47 @@ const OtherMainPage = () => { } }, [userId]); return ( -
+
postFriendRequest(userId)} /> - +
+
+ {userInfo?.nickname}님의 +
+
+ 홈에 오신 걸 환영해요! +
+
+ + 지금까지의 기록 + +
+ +
+ 개 +
+
+
+ + + +
); }; diff --git a/bookduck/src/pages/OtherUserPage/OtherStatisticsPage.jsx b/bookduck/src/pages/OtherUserPage/OtherStatisticsPage.jsx new file mode 100644 index 00000000..5cab5603 --- /dev/null +++ b/bookduck/src/pages/OtherUserPage/OtherStatisticsPage.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +const OtherStatisticsPage = () => { + const navigate = useNavigate(); + let { id: userId } = useParams(); + return
; +}; + +export default OtherStatisticsPage; diff --git a/bookduck/src/pages/SearchPage/SearchMainPage.jsx b/bookduck/src/pages/SearchPage/SearchMainPage.jsx index dc1f6492..10c71dbf 100644 --- a/bookduck/src/pages/SearchPage/SearchMainPage.jsx +++ b/bookduck/src/pages/SearchPage/SearchMainPage.jsx @@ -33,7 +33,7 @@ const SearchMainPage = () => { const getPopularBooks = async () => { try { const response = await get(`/bookinfo/most`); - console.log("많이 읽는", response); + // console.log("많이 읽는", response); setPopularBooks(response.bookList); } catch (error) { console.error("많이 읽는 책 읽기 오류", error); diff --git a/bookduck/src/pages/SettingPage/SettingPage.jsx b/bookduck/src/pages/SettingPage/SettingPage.jsx index 35ceeff4..7425dc2e 100644 --- a/bookduck/src/pages/SettingPage/SettingPage.jsx +++ b/bookduck/src/pages/SettingPage/SettingPage.jsx @@ -215,7 +215,7 @@ const SettingPage = () => { const handleLogout = async () => { try { await postLogout(); - localStorage.removeItem("token"); + localStorage.clear(); navigate(`/login`, { replace: true }); } catch (error) { console.error(error); diff --git a/bookduck/yarn.lock b/bookduck/yarn.lock index c20e7d81..762ea274 100644 --- a/bookduck/yarn.lock +++ b/bookduck/yarn.lock @@ -1986,17 +1986,17 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@tanstack/query-core@5.59.20": - version "5.59.20" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.59.20.tgz#356718976536727b9af0ad1163a21fd6a44ee0a9" - integrity sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg== +"@tanstack/query-core@5.60.6": + version "5.60.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.60.6.tgz#0dd33fe231b0d18bf66d0c615b29899738300658" + integrity sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ== -"@tanstack/react-query@^5.60.2": - version "5.60.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.60.2.tgz#a65db96702c11f3f868c40372ce66f05c2a77cc6" - integrity sha512-JhpJNxIAPuE0YCpP1Py4zAsgx+zY0V531McRMtQbwVlJF8+mlZwcOPrzGmPV248K8IP+mPbsfxXToVNMNwjUcw== +"@tanstack/react-query@^5.61.3": + version "5.61.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.61.3.tgz#0187b73b87adaeaed09f3d9717e35b507175fe23" + integrity sha512-c3Oz9KaCBapGkRewu7AJLhxE9BVqpMcHsd3KtFxSd7FSCu2qGwqfIN37zbSGoyk6Ix9LGZBNHQDPI6GpWABnmA== dependencies: - "@tanstack/query-core" "5.59.20" + "@tanstack/query-core" "5.60.6" "@trysound/sax@0.2.0": version "0.2.0"