From 15404e9bd6784df8ae425ab4c1dcbd35604e8f7f Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:28:20 +0900 Subject: [PATCH 01/46] =?UTF-8?q?[=EA=B9=80=EC=9C=A0=EC=A7=84]=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EA=B5=AC=20List?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=95=9C=EB=B2=88=EC=97=90=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC,=20lastProductRef=20=EC=A1=B0=EA=B1=B4=EB=AC=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductSearchResultPage.js | 2 - src/pages/product/ProductsPage.js | 2 - src/pages/product/components/ProductList.js | 76 ++++++++++---------- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/pages/product/ProductSearchResultPage.js b/src/pages/product/ProductSearchResultPage.js index de81969..4536b15 100644 --- a/src/pages/product/ProductSearchResultPage.js +++ b/src/pages/product/ProductSearchResultPage.js @@ -98,8 +98,6 @@ const ProductSearchResultPage = () => { hasMore={hasMore} /> - {loading &&
로딩 중...
} - {!loading && !hasMore &&
더 이상 아이템이 없습니다.
}
); diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index ddd28ad..7770a60 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -79,8 +79,6 @@ export default function ProductsPage() { hasMore={hasMore} /> - {loading &&
로딩 중...
} - {!loading && !hasMore &&
더 이상 아이템이 없습니다.
}
); diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 16ff02d..6431474 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -51,43 +51,33 @@ const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { return () => { window.removeEventListener("scroll", handleScroll); }; - }, []); + }, [productList]); return ( {productList && productList.length > 0 ? ( <> - {productList.map((product, index) => { - if (productList.length === index + 1) { - return ( - handleProductPage(product.id)} - > - {/* 추후에 상품 디테일 페이지에 연결 ! */} - + {productList.map((product, index) => ( + handleProductPage(product.id)} + > + {/* 추후에 상품 디테일 페이지에 연결 ! */} + + {index === productList.length - 1 ? ( + {product.title} + ) : ( + <> + {product.location} {product.title} - - ); - } else { - return ( - handleProductPage(product.id)} - /* 추후에 상품 디테일 페이지에 연결 ! */ - > - - - {product.content} - {product.title} - - ); - } - })} + {product.price}원 + + )} + + ))} { ) : ( - {productList ? "상품이 없습니다." : "상품을 불러오는 중입니다..."} + {!loading && productList + ? "상품이 없습니다." + : "상품을 불러오는 중입니다..."} )} @@ -114,37 +106,47 @@ const ProductListWrap = styled.div` const CardWrap = styled.div` display: flex; flex-wrap: wrap; + justify-content: space-around; + margin-bottom: 30px; `; const Card = styled.div` - width: 48%; - margin: 1%; + width: 180px; + margin-bottom: 10px; padding: 10px; box-sizing: border-box; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); `; const CardImg = styled.img` + width: 160px; + height: 120px; + background-size: cover; border-radius: 12px; margin-bottom: 5px; `; const ProductBadge = styled.div` - width: 100px; - height: 24px; + width: 50px; + height: 20px; background-color: #5cb8bc; color: white; - font-size: 14px; + font-size: 11px; display: flex; align-items: center; justify-content: center; border-radius: 8px; - margin-bottom: 4px; + margin: 10px 0 0 0; +`; + +const ProductPrice = styled.div` + font-size: 14px; `; const ProductTitle = styled.h1` font-size: 16px; font-weight: 700; + margin: 5px 0; `; const NoticeMsg = styled.p` From a1b97d1660af6dcc9b5f14b06e854b69f71752ba Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:10:59 +0900 Subject: [PATCH 02/46] =?UTF-8?q?refactor:=20=EC=B5=9C=EA=B7=BC=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=20=ED=86=A0=ED=81=B0=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductSearchResultPage.js | 2 +- src/pages/product/ProductsPage.js | 2 +- src/pages/product/ProductsSearchPage.js | 92 +++++++++++++------ .../product/components/RecentSearches.js | 31 ++++--- 4 files changed, 85 insertions(+), 42 deletions(-) diff --git a/src/pages/product/ProductSearchResultPage.js b/src/pages/product/ProductSearchResultPage.js index 4536b15..2af31ba 100644 --- a/src/pages/product/ProductSearchResultPage.js +++ b/src/pages/product/ProductSearchResultPage.js @@ -28,7 +28,7 @@ const ProductSearchResultPage = () => { direction: "asc" }); - if (resData.data.data.length > 0) { + if (resData.data?.data?.length > 0) { setProductList((prevList) => [...prevList, ...resData.data.data]); setPage(page + 1); } else { diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index 7770a60..e9b877a 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -28,7 +28,7 @@ export default function ProductsPage() { direction: "asc" }); - if (resData.data.data.length > 0) { + if (resData.data?.data?.length > 0) { setProductList((prevList) => [...prevList, ...resData.data.data]); setPage(page + 1); } else { diff --git a/src/pages/product/ProductsSearchPage.js b/src/pages/product/ProductsSearchPage.js index 43a5b50..b2df4bd 100644 --- a/src/pages/product/ProductsSearchPage.js +++ b/src/pages/product/ProductsSearchPage.js @@ -2,44 +2,74 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import RecentSearches from "./components/RecentSearches"; +import useAuthStore from "@/utils/hooks/store/useAuthStore"; +import { getUsersMe } from "@/api/marketApi"; const ProductsSearchPage = () => { const navigate = useNavigate(); + const { isAuthenticated, accessToken } = useAuthStore(); const [inputVal, setInputVal] = useState(""); const [searchTerm, setSearchTerm] = useState(""); + const saveSearchKeyword = (word) => { + // 최근 검색어 저장 로직 추가 + const existingSearches = + JSON.parse(localStorage.getItem("RecentSearches")) || []; + + let updatedSearches; + + if (isAuthenticated) { + // 로그인 한 사용자일 때 + const existingTokenIndex = existingSearches.findIndex( + (item) => item.token === accessToken + ); + + if (existingTokenIndex !== -1) { + // 해당 토큰이 이미 존재하는 경우(검색 기록 O) + updatedSearches = existingSearches.map((item, index) => + index === existingTokenIndex + ? { + token: accessToken, + keyword: [word, ...item.keyword] + .filter((keyword, i, self) => self.indexOf(keyword) === i) + .slice(0, 4) + } + : item + ); + } else { + // 로그인 & 이전 최근 검색 기록 없는 경우 + updatedSearches = [ + ...existingSearches, + { token: accessToken, keyword: [word] } + ]; + } + } else { + // 미로그인 + updatedSearches = existingSearches.map((item) => item); + } + + localStorage.setItem("RecentSearches", JSON.stringify(updatedSearches)); + }; + const handleSearchSubmit = (current) => { let searchWord; console.log("type::", typeof current); if (typeof current === "string") { // 최근 검색어를 클릭한 경우 - console.log("최근 검색어", current); searchWord = current.trim(); } else { if (inputVal === "") { // 검색어가 빈 값인 경우 처리하지 않음 - console.log("빈 값을 입력하고 검색하는 경우 처리하지 않음"); // 에러 팝업 또는 다른 처리를 여기에 추가 return; } // 검색 버튼을 클릭한 경우 - console.log("검색 버튼"); searchWord = inputVal; } - // 최근 검색어 저장 로직 추가 - const existingSearches = - JSON.parse(localStorage.getItem("RecentSearches")) || []; - - // 최근 검색어 목록 업데이트 - const updatedSearches = [ - searchWord, - ...existingSearches.filter((item) => item !== searchWord).slice(0, 3) - ]; - localStorage.setItem("RecentSearches", JSON.stringify(updatedSearches)); - - console.log("handleSearchSubmit inputVal::", searchWord); + // 최근 검색어 저장 + saveSearchKeyword(searchWord); // 검색어 업데이트 및 초기화 + 메뉴 이동 navigate(`/web/search/${encodeURIComponent(searchWord)}`); @@ -47,31 +77,35 @@ const ProductsSearchPage = () => { setInputVal(""); }; + const onSubmit = (e) => { + e.preventDefault(); + if (inputVal) handleSearchSubmit(); + }; + const handleSearchChange = (e) => { // 검색어 입력이 있을 때만 동작 - setInputVal(e.target.value); }; - useEffect(() => { - console.log("검색된 값::", searchTerm); - }, [searchTerm]); + useEffect(() => {}, [searchTerm]); return (
-
- -
+
+
+ +
+
diff --git a/src/pages/product/components/RecentSearches.js b/src/pages/product/components/RecentSearches.js index 5afab59..ea11d15 100644 --- a/src/pages/product/components/RecentSearches.js +++ b/src/pages/product/components/RecentSearches.js @@ -1,20 +1,29 @@ +import useAuthStore from "@/utils/hooks/store/useAuthStore"; import styled from "@emotion/styled"; +import { UsersIcon } from "@heroicons/react/16/solid"; import { useEffect, useState } from "react"; // 검색 - 최근 검색어 목록 컴포넌트 const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { + const { isAuthenticated, accessToken } = useAuthStore(); const [recentSearches, setRecentSearches] = useState([]); - const handleClearRecentSearches = () => { - localStorage.removeItem("RecentSearches"); - setRecentSearches([]); - }; - useEffect(() => { - const storedSearches = - JSON.parse(localStorage.getItem("RecentSearches")) || []; + let storedSearches = []; + + if (isAuthenticated && accessToken) { + // 로그인 & 토큰 유효 사용자일 때 + const userTokenSearches = + (JSON.parse(localStorage.getItem("RecentSearches")) || [])[0] || {}; + + storedSearches = userTokenSearches.keyword || []; + } else { + // 미로그인 사용자일 때 + storedSearches = []; + } + setRecentSearches(storedSearches); - }, [searchTerm]); + }, [searchTerm, isAuthenticated, accessToken]); return ( <> @@ -22,12 +31,12 @@ const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { 최근 검색어
- {recentSearches.map((search, index) => ( + {recentSearches.map((word, index) => ( handleSearchSubmit(search)}> - {search.length > 6 ? `${search.slice(0, 5)}..` : search} + onClick={() => handleSearchSubmit(word)}> + {word.length > 6 ? `${word.slice(0, 5)}..` : word} ))}
From 773dd6eb17bc45733b5675b1ba5a5b5176ec8e76 Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:35:23 +0900 Subject: [PATCH 03/46] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 6431474..9ddf500 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,10 +1,12 @@ import styled from "@emotion/styled"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { // Intersection Observer를 사용하여 마지막 상품이 보여질 때를 감지 + const navigate = useNavigate(); const observer = useRef(); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 const [showTopButton, setShowTopButton] = useState(false); @@ -46,6 +48,7 @@ const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { }; useEffect(() => { + console.log("list::", productList); window.addEventListener("scroll", handleScroll); return () => { @@ -63,8 +66,7 @@ const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { key={index} ref={index === productList.length - 1 ? lastProductRef : null} className="productCard" - // onClick={() => handleProductPage(product.id)} - > + onClick={() => navigate(`/web/product/${product.id}`)}> {/* 추후에 상품 디테일 페이지에 연결 ! */} {index === productList.length - 1 ? ( From 5ea4b5263a6fbc39b79e8a1c6d13653927951e7f Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:11:52 +0900 Subject: [PATCH 04/46] =?UTF-8?q?refactor:=20react-query=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20productList=20=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductSearchResultPage.js | 86 +------------- src/pages/product/ProductsPage.js | 72 +----------- src/pages/product/components/ProductList.js | 117 ++++++++++++++----- 3 files changed, 88 insertions(+), 187 deletions(-) diff --git a/src/pages/product/ProductSearchResultPage.js b/src/pages/product/ProductSearchResultPage.js index 2af31ba..851262b 100644 --- a/src/pages/product/ProductSearchResultPage.js +++ b/src/pages/product/ProductSearchResultPage.js @@ -1,88 +1,11 @@ // ProductSearchResult.js import styled from "@emotion/styled"; -import { getPublishedPosts } from "@/api/marketApi"; -import { useState } from "react"; -import { useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import ProductList from "./components/ProductList"; const ProductSearchResultPage = () => { const { keyword } = useParams(); // 검색어 - const [productList, setProductList] = useState(); // 넘겨줄 상품 리스트 배열 - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [page, setPage] = useState(1); - const loaderRef = useRef(null); - - const fetchMoreData = async () => { - if (loading || !hasMore) return; - - try { - setLoading(true); - const resData = await getPublishedPosts({ - page: page !== 1 ? page + 1 : page, - limit: 16, - query: keyword ? keyword : "", - orderBy: "createdAt", - direction: "asc" - }); - - if (resData.data?.data?.length > 0) { - setProductList((prevList) => [...prevList, ...resData.data.data]); - setPage(page + 1); - } else { - setHasMore(false); - } - } catch (error) { - console.error("데이터 추가 불러오기 실패:", error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - searchApi(keyword); - }, [keyword]); - - useEffect(() => { - const handleScroll = () => { - if (loaderRef.current) { - const { scrollTop, clientHeight, scrollHeight } = loaderRef.current; - - if (scrollHeight - scrollTop === clientHeight) { - fetchMoreData(); - } - } - }; - - if (loaderRef.current) { - // 값이 없는 경우(null) 페이지 이동 시 에러 발생하기 때문에 null이 아닐때만 동작하도록 함 - loaderRef.current.addEventListener("scroll", handleScroll); - } - - return () => { - if (loaderRef.current) { - loaderRef.current.removeEventListener("scroll", handleScroll); - } - }; - }, [fetchMoreData]); - - const searchApi = async (keyword) => { - try { - const resData = await getPublishedPosts({ - page: 1, - limit: 10, - query: keyword ? keyword : "", - orderBy: "createdAt", - direction: "asc" - }); - - setProductList(resData.data.data); - } catch (error) { - console.error("검색 API 호출 실패:", error); - } - }; return (
@@ -91,14 +14,7 @@ const ProductSearchResultPage = () => { 결과입니다. - - -
+
); }; diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index e9b877a..686edda 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -1,85 +1,15 @@ -import { useEffect } from "react"; import ProductList from "./components/ProductList"; import styled from "@emotion/styled"; -import { useState } from "react"; -import { useRef } from "react"; -import { getPublishedPosts } from "@/api/marketApi"; // 처음 진입하면 보이는 메인 페이지 // api를 호출하여 ProcudtList에 정보를 넘겨서 보여주도록 함 // 스크롤을 통해 더 많은 상품을 동적으로 로드 export default function ProductsPage() { - const [productList, setProductList] = useState([]); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [page, setPage] = useState(1); - const loaderRef = useRef(null); - - const fetchMoreData = async () => { - if (loading || !hasMore) return; - - try { - setLoading(true); - const resData = await getPublishedPosts({ - page: page !== 1 ? page + 1 : page, - limit: 10, - query: "", - orderBy: "createdAt", - direction: "asc" - }); - - if (resData.data?.data?.length > 0) { - setProductList((prevList) => [...prevList, ...resData.data.data]); - setPage(page + 1); - } else { - setHasMore(false); - } - } catch (error) { - console.error("데이터 추가 불러오기 실패:", error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - const handleScroll = () => { - if (loaderRef.current) { - const { scrollTop, clientHeight, scrollHeight } = loaderRef.current; - - if (scrollHeight - scrollTop === clientHeight) { - fetchMoreData(); - } - } - }; - - if (loaderRef.current) { - // 값이 없는 경우(null) 페이지 이동 시 에러 발생하기 때문에 null이 아닐때만 동작하도록 함 - loaderRef.current.addEventListener("scroll", handleScroll); - } - - return () => { - if (loaderRef.current) { - loaderRef.current.removeEventListener("scroll", handleScroll); - } - }; - }, [fetchMoreData]); - - useEffect(() => { - fetchMoreData(); - }, []); - return (
원하시는 상품을 찾아보세요. - - -
+
); } diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 9ddf500..477807e 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,60 +1,121 @@ +import { getPublishedPosts } from "@/api/marketApi"; import styled from "@emotion/styled"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 -const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { +const ProductList = (props) => { // Intersection Observer를 사용하여 마지막 상품이 보여질 때를 감지 + const { keyword } = props; const navigate = useNavigate(); + const [productList, setProductList] = useState([]); + const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 const observer = useRef(); - // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 - const [showTopButton, setShowTopButton] = useState(false); - // 마지막 상품에 대한 콜백, Intersection Observer와 함께 사용 + const getProductList = async ({ currentPage = 1, queryKey }) => { + const { keyword } = queryKey[1]; // Destructure the parameters from the queryKey + console.log("kyj currentPage::", currentPage); + const resData = await getPublishedPosts({ + page: currentPage !== 1 ? currentPage + 1 : currentPage, + limit: 10, + query: keyword ? keyword : "", + orderBy: "createdAt", + direction: "asc" + }); + console.log("kyj resData::", resData); + + const { page, lastPage, data: responseData } = resData.data; + // api 호출 값에서 가지고 온 lastPage 결과 + + setProductList((prevList) => [...prevList, ...responseData]); + + return page < lastPage ? page + 1 : 1; + }; + + const { data, error, fetchNextPage, hasNextPage, isFetching } = + useInfiniteQuery({ + queryKey: ["productList", { keyword: keyword || "" }], + queryFn: getProductList, + getNextPageParam: (curPage) => curPage + }); + + const observerOption = { + root: null, + threshold: 0.5, + rootMargin: "0px" + }; + const lastProductRef = useCallback( (node) => { - if (loading || !hasMore) return; - if (observer.current) { observer.current.disconnect(); } observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { - fetchMoreData(); + if (entries[0].isIntersecting && hasNextPage) { + setShowTopButton(true); + fetchNextPage(); } - }); + }, observerOption); if (node) { observer.current.observe(node); } }, - [loading, hasMore, fetchMoreData] + [fetchNextPage, hasNextPage] ); - const handleScroll = () => { - const scrollY = window.scrollY; + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth" + }); + }; + + // useEffect(() => { + // const handleTop = () => { + // // 스크롤이 제일 상단에 가면 top 버튼 안 보이게 처리 + // const isTop = window.scrollY === 0; + // setShowTopButton(!isTop); + // }; - if (scrollY > 300) { - setShowTopButton(true); - } else { - setShowTopButton(false); + // window.addEventListener("scroll", handleTop); + + // return () => { + // window.removeEventListener("scroll", handleTop); + // }; + // }, []); + + useEffect(() => { + if (error) { + console.error("데이터 추가 불러오기 실패:", error); } - }; - const scrollToTop = () => { - window.scrollTo({ top: 0, behavior: "smooth" }); - }; + if (isFetching) { + console.log("로딩 중"); + // return
로딩 중입니다.....
; + // 로딩 중인 상태를 표시하는 UI + } + }, [error, data, isFetching]); useEffect(() => { - console.log("list::", productList); - window.addEventListener("scroll", handleScroll); + // 초기 데이터 로딩 + let searchWord = keyword || ""; + + const handleTop = () => { + // 스크롤이 제일 상단에 가면 top 버튼 안 보이게 처리 + const isTop = window.scrollY === 0; + setShowTopButton(!isTop); + }; + + window.addEventListener("scroll", handleTop); return () => { - window.removeEventListener("scroll", handleScroll); + window.removeEventListener("scroll", handleTop); }; - }, [productList]); + }, [keyword]); return ( @@ -65,9 +126,7 @@ const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { navigate(`/web/product/${product.id}`)}> - {/* 추후에 상품 디테일 페이지에 연결 ! */} {index === productList.length - 1 ? ( {product.title} @@ -89,11 +148,7 @@ const ProductList = ({ productList, fetchMoreData, loading, hasMore }) => { ) : ( - - {!loading && productList - ? "상품이 없습니다." - : "상품을 불러오는 중입니다..."} - + {!productList && "상품이 없습니다."} )} ); From 7afe926cf0cb106ff11cbe200d75d426dfc5ee59 Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:07:55 +0900 Subject: [PATCH 05/46] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=82=AC=EC=9A=A9=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80=20=EC=8B=9C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EA=B3=B5=EC=9A=A9=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 13 ++ src/components/Loading.js | 19 +++ src/pages/product/ProductsSearchPage.js | 46 ++---- src/pages/product/components/ProductList.js | 137 ++++++++--------- .../product/components/RecentSearches.js | 17 +-- .../product/components/SaveSearchStorage.js | 143 ++++++++++++++++++ 7 files changed, 258 insertions(+), 118 deletions(-) create mode 100644 src/components/Loading.js create mode 100644 src/pages/product/components/SaveSearchStorage.js diff --git a/package.json b/package.json index 7b5069f..52839d1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.1", + "react-spinners": "^0.13.8", "swiper": "^11.0.5", "zustand": "^4.4.7" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c991733..1ce184b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: react-router-dom: specifier: ^6.21.1 version: 6.21.1(react-dom@18.2.0)(react@18.2.0) + react-spinners: + specifier: ^0.13.8 + version: 0.13.8(react-dom@18.2.0)(react@18.2.0) swiper: specifier: ^11.0.5 version: 11.0.5 @@ -4823,6 +4826,16 @@ packages: react: 18.2.0 dev: false + /react-spinners@0.13.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/components/Loading.js b/src/components/Loading.js new file mode 100644 index 0000000..59a78c1 --- /dev/null +++ b/src/components/Loading.js @@ -0,0 +1,19 @@ +import React from "react"; +import { BeatLoader } from "react-spinners"; +import styled from "@emotion/styled"; + +const Loading = () => { + return ( + + + + ); +}; +export default Loading; + +const LoadingWrap = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; diff --git a/src/pages/product/ProductsSearchPage.js b/src/pages/product/ProductsSearchPage.js index b2df4bd..e539875 100644 --- a/src/pages/product/ProductsSearchPage.js +++ b/src/pages/product/ProductsSearchPage.js @@ -1,9 +1,11 @@ import { useEffect, useState } from "react"; - import { useNavigate } from "react-router-dom"; import RecentSearches from "./components/RecentSearches"; import useAuthStore from "@/utils/hooks/store/useAuthStore"; -import { getUsersMe } from "@/api/marketApi"; +import { + saveLocalStorage, + saveCacheStorage +} from "@/pages/product/components/SaveSearchStorage"; const ProductsSearchPage = () => { const navigate = useNavigate(); @@ -13,42 +15,12 @@ const ProductsSearchPage = () => { const saveSearchKeyword = (word) => { // 최근 검색어 저장 로직 추가 - const existingSearches = - JSON.parse(localStorage.getItem("RecentSearches")) || []; - - let updatedSearches; - - if (isAuthenticated) { - // 로그인 한 사용자일 때 - const existingTokenIndex = existingSearches.findIndex( - (item) => item.token === accessToken - ); - - if (existingTokenIndex !== -1) { - // 해당 토큰이 이미 존재하는 경우(검색 기록 O) - updatedSearches = existingSearches.map((item, index) => - index === existingTokenIndex - ? { - token: accessToken, - keyword: [word, ...item.keyword] - .filter((keyword, i, self) => self.indexOf(keyword) === i) - .slice(0, 4) - } - : item - ); - } else { - // 로그인 & 이전 최근 검색 기록 없는 경우 - updatedSearches = [ - ...existingSearches, - { token: accessToken, keyword: [word] } - ]; - } - } else { - // 미로그인 - updatedSearches = existingSearches.map((item) => item); + // 로컬 스토리지 사용할 수 없을 때 브라우저 캐시 사용 + try { + saveLocalStorage(word, isAuthenticated, accessToken); + } catch (error) { + saveCacheStorage(word, isAuthenticated, accessToken); } - - localStorage.setItem("RecentSearches", JSON.stringify(updatedSearches)); }; const handleSearchSubmit = (current) => { diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 477807e..70045e7 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,4 +1,6 @@ import { getPublishedPosts } from "@/api/marketApi"; +import Loading from "@/components/Loading"; +import useModalStore from "@/utils/hooks/store/useModalStore"; import styled from "@emotion/styled"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -7,16 +9,15 @@ import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 const ProductList = (props) => { - // Intersection Observer를 사용하여 마지막 상품이 보여질 때를 감지 const { keyword } = props; const navigate = useNavigate(); const [productList, setProductList] = useState([]); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 + const { openModal, closeModal } = useModalStore(); const observer = useRef(); const getProductList = async ({ currentPage = 1, queryKey }) => { - const { keyword } = queryKey[1]; // Destructure the parameters from the queryKey - console.log("kyj currentPage::", currentPage); + const { keyword } = queryKey[1]; const resData = await getPublishedPosts({ page: currentPage !== 1 ? currentPage + 1 : currentPage, limit: 10, @@ -24,22 +25,18 @@ const ProductList = (props) => { orderBy: "createdAt", direction: "asc" }); - console.log("kyj resData::", resData); const { page, lastPage, data: responseData } = resData.data; - // api 호출 값에서 가지고 온 lastPage 결과 - setProductList((prevList) => [...prevList, ...responseData]); return page < lastPage ? page + 1 : 1; }; - const { data, error, fetchNextPage, hasNextPage, isFetching } = - useInfiniteQuery({ - queryKey: ["productList", { keyword: keyword || "" }], - queryFn: getProductList, - getNextPageParam: (curPage) => curPage - }); + const { error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ + queryKey: ["productList", { keyword: keyword || "" }], + queryFn: getProductList, + getNextPageParam: (curPage) => curPage + }); const observerOption = { root: null, @@ -48,6 +45,7 @@ const ProductList = (props) => { }; const lastProductRef = useCallback( + // Intersection Observer를 사용하여 마지막 상품이 보여질 때를 감지 (node) => { if (observer.current) { observer.current.disconnect(); @@ -55,6 +53,7 @@ const ProductList = (props) => { observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasNextPage) { + // 현재 ref가 활성화 상태 && 다음 페이지가 있는 경우 setShowTopButton(true); fetchNextPage(); } @@ -68,41 +67,15 @@ const ProductList = (props) => { ); const scrollToTop = () => { + // top 버튼 누르면 최상단으로 이동 window.scrollTo({ top: 0, behavior: "smooth" }); }; - // useEffect(() => { - // const handleTop = () => { - // // 스크롤이 제일 상단에 가면 top 버튼 안 보이게 처리 - // const isTop = window.scrollY === 0; - // setShowTopButton(!isTop); - // }; - - // window.addEventListener("scroll", handleTop); - - // return () => { - // window.removeEventListener("scroll", handleTop); - // }; - // }, []); - - useEffect(() => { - if (error) { - console.error("데이터 추가 불러오기 실패:", error); - } - - if (isFetching) { - console.log("로딩 중"); - // return
로딩 중입니다.....
; - // 로딩 중인 상태를 표시하는 UI - } - }, [error, data, isFetching]); - useEffect(() => { // 초기 데이터 로딩 - let searchWord = keyword || ""; const handleTop = () => { // 스크롤이 제일 상단에 가면 top 버튼 안 보이게 처리 @@ -117,40 +90,62 @@ const ProductList = (props) => { }; }, [keyword]); + useEffect(() => { + // 추후에 조건 풀 지 확인해보기 + if (error && !error.response?.status === 401) { + console.error("상품 불러오기 실패:", error); + const customContent = ( +
+

상품을 가져올 수 없습니다.

+

다시 시도해 주시기 바랍니다.

+
+ +
+
+ ); + openModal(customContent); + } + }, [error]); + return ( - - {productList && productList.length > 0 ? ( - <> - - {productList.map((product, index) => ( - navigate(`/web/product/${product.id}`)}> - - {index === productList.length - 1 ? ( +
+ + {isFetching && } + {productList && productList.length > 0 ? ( + <> + + {productList.map((product, index) => ( + navigate(`/web/product/${product.id}`)}> + + {product.location} {product.title} - ) : ( - <> - {product.location} - {product.title} - {product.price}원 - - )} - - ))} - - - TOP - - - ) : ( - {!productList && "상품이 없습니다."} - )} - + {product.price}원 + + ))} + + {} : scrollToTop}> + TOP + + + ) : ( +
+ {!productList?.length && !isFetching && ( + + {`${keyword ? "검색 결과가" : "상품이"} 없습니다.`} + + )} +
+ )} + +
); }; diff --git a/src/pages/product/components/RecentSearches.js b/src/pages/product/components/RecentSearches.js index ea11d15..3d410b0 100644 --- a/src/pages/product/components/RecentSearches.js +++ b/src/pages/product/components/RecentSearches.js @@ -1,7 +1,8 @@ import useAuthStore from "@/utils/hooks/store/useAuthStore"; import styled from "@emotion/styled"; -import { UsersIcon } from "@heroicons/react/16/solid"; import { useEffect, useState } from "react"; +import { getLocalStorage } from "@/pages/product/components/SaveSearchStorage"; +import { getCacheStorage } from "@/pages/product/components/SaveSearchStorage"; // 검색 - 최근 검색어 목록 컴포넌트 const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { @@ -11,15 +12,11 @@ const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { useEffect(() => { let storedSearches = []; - if (isAuthenticated && accessToken) { - // 로그인 & 토큰 유효 사용자일 때 - const userTokenSearches = - (JSON.parse(localStorage.getItem("RecentSearches")) || [])[0] || {}; - - storedSearches = userTokenSearches.keyword || []; - } else { - // 미로그인 사용자일 때 - storedSearches = []; + try { + // 로컬 스토리지 사용할 수 없을 때 브라우저 캐시 사용 + storedSearches = getLocalStorage(isAuthenticated, accessToken); + } catch (error) { + storedSearches = getCacheStorage(isAuthenticated, accessToken); } setRecentSearches(storedSearches); diff --git a/src/pages/product/components/SaveSearchStorage.js b/src/pages/product/components/SaveSearchStorage.js new file mode 100644 index 0000000..f5a99c1 --- /dev/null +++ b/src/pages/product/components/SaveSearchStorage.js @@ -0,0 +1,143 @@ +// 임시로 사용할 브라우저 캐시 스토리지 선언 +export const cacheStorage = { + cache: {}, + getItem(key) { + return this.cache[key] || null; + }, + setItem(key, value) { + this.cache[key] = value; + }, + removeItem(key) { + delete this.cache[key]; + }, + clear() { + this.cache = {}; + } +}; + +// 로컬 스토리지에 최근 검색어 저장 +export const saveLocalStorage = (word, isAuthenticated, accessToken) => { + let updatedSearches; + + // 로컬 스토리지에서 최근 검색어 가져옴 + const existingSearches = + JSON.parse(localStorage.getItem("RecentSearches")) || []; + + if (isAuthenticated) { + // 로그인 되어있는 경우, 저장된 스토리지의 토큰과 로그인한 유저의 토큰 값 비교 + const existingTokenIndex = existingSearches.findIndex( + (item) => item.token === accessToken + ); + + if (existingTokenIndex !== -1) { + // 이미 해당 토큰이 존재하는 경우, 해당 토큰의 검색어 업데이트 + updatedSearches = existingSearches.map((item, index) => + index === existingTokenIndex + ? { + token: accessToken, + // 새로운 검색어를 추가하고 중복 제거 후 최대 4개까지 유지 + keyword: [word, ...item.keyword] + .filter((keyword, i, self) => self.indexOf(keyword) === i) + .slice(0, 4) + } + : item + ); + } else { + // 해당 토큰이 존재하지 않는 경우, 새로운 토큰과 검색어를 추가 + updatedSearches = [ + ...existingSearches, + { token: accessToken, keyword: [word] } + ]; + } + } else { + return; + } + + localStorage.setItem("RecentSearches", JSON.stringify(updatedSearches)); +}; + +// 로컬 스토리지에 저장된 최근 검색어 조회 +export const getLocalStorage = (isAuthenticated, accessToken) => { + let storedSearches = []; + + if (isAuthenticated && accessToken) { + // 로그인 및 유효한 토큰을 가진 사용자인 경우 + + // 로컬 스토리지에서 "RecentSearches" 키의 값을 가져와 파싱 + const userTokenSearches = + (JSON.parse(localStorage.getItem("RecentSearches")) || [])[0] || {}; + + // 해당 토큰의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 + storedSearches = userTokenSearches.keyword || []; + } + // 최종적으로 로그인 상태가 아니거나 토큰이 유효하지 않은 경우 빈 배열 반환 + return storedSearches; +}; + +// 캐시 스토리지에 최근 검색어 저장 +export const saveCacheStorage = (word, isAuthenticated, accessToken) => { + let updatedSearches; + + const existingSearches = + JSON.parse(cacheStorage.getItem("RecentSearches")) || []; + + if (isAuthenticated) { + // 로그인 되어있는 경우, 저장된 스토리지의 토큰과 로그인한 유저의 토큰 값 비교 + const existingTokenIndex = existingSearches.findIndex( + (item) => item.token === accessToken + ); + + if (existingTokenIndex !== -1) { + // 이미 해당 토큰이 존재하는 경우, 해당 토큰의 검색어 업데이트 + updatedSearches = existingSearches.map((item, index) => + index === existingTokenIndex + ? { + token: accessToken, + // 새로운 검색어를 추가하고 중복 제거 후 최대 4개까지 유지 + keyword: [word, ...item.keyword] + .filter((keyword, i, self) => self.indexOf(keyword) === i) + .slice(0, 4) + } + : item + ); + } else { + // 해당 토큰이 존재하지 않는 경우, 새로운 토큰과 검색어를 추가 + updatedSearches = [ + ...existingSearches, + { token: accessToken, keyword: [word] } + ]; + } + } else { + return; + } + + cacheStorage.setItem("RecentSearches", JSON.stringify(updatedSearches)); +}; + +// 캐시 스토리지에 저장된 최근 검색어 조회 +export const getCacheStorage = (isAuthenticated, accessToken) => { + let storedSearches = []; + + if (isAuthenticated && accessToken) { + // 로그인 및 유효한 토큰을 가진 사용자인 경우 + + // 캐시 스토리지에서 "RecentSearches" 키의 값을 가져와 파싱 + const userTokenSearches = + (JSON.parse(cacheStorage.getItem("RecentSearches")) || [])[0] || {}; + + // 해당 토큰의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 + storedSearches = userTokenSearches.keyword || []; + } + // 최종적으로 로그인 상태가 아니거나 토큰이 유효하지 않은 경우 빈 배열 반환 + return storedSearches; +}; + +const SaveSearchStorage = { + cacheStorage, + saveLocalStorage, + getLocalStorage, + saveCacheStorage, + getCacheStorage +}; + +export { SaveSearchStorage }; From 7fefa5ab085a1764533c127e45fd9bcfe0f609c6 Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:34:23 +0900 Subject: [PATCH 06/46] =?UTF-8?q?fix:=20=EC=83=81=ED=92=88=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20useInfiniteQuery=EC=97=90=EC=84=9C=20pageP?= =?UTF-8?q?aram=20+1=20=EC=95=88=20=EB=90=98=EA=B3=A0=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 70045e7..f1191cb 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -7,7 +7,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 - const ProductList = (props) => { const { keyword } = props; const navigate = useNavigate(); @@ -16,11 +15,11 @@ const ProductList = (props) => { const { openModal, closeModal } = useModalStore(); const observer = useRef(); - const getProductList = async ({ currentPage = 1, queryKey }) => { - const { keyword } = queryKey[1]; + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) const resData = await getPublishedPosts({ - page: currentPage !== 1 ? currentPage + 1 : currentPage, - limit: 10, + page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 + limit: 20, query: keyword ? keyword : "", orderBy: "createdAt", direction: "asc" @@ -29,13 +28,20 @@ const ProductList = (props) => { const { page, lastPage, data: responseData } = resData.data; setProductList((prevList) => [...prevList, ...responseData]); - return page < lastPage ? page + 1 : 1; + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; }; const { error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ - queryKey: ["productList", { keyword: keyword || "" }], + queryKey: ["productList"], queryFn: getProductList, - getNextPageParam: (curPage) => curPage + // 위의 getPublishedPosts 결과값으로 얻은 page (현재 받아온 페이지) , lastpage (총 페이지) + getNextPageParam: ({ curPage, lastPage }) => { + // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 + // 여기서 return 하는 값은 pageParam으로 전달 됨 + return curPage < lastPage ? curPage + 1 : lastPage; + } }); const observerOption = { From bcbc8116703d5f89fface3c9375270052f95a839 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Thu, 1 Feb 2024 17:00:15 +0900 Subject: [PATCH 07/46] =?UTF-8?q?feat:=20=EC=B5=9C=EC=83=81=EB=8B=A8=20("/?= =?UTF-8?q?")=EA=B2=BD=EB=A1=9C=EC=97=90=EC=84=9C=20location=20=EA=B0=90?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=9C=84=ED=95=9C=20router=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 31 ++++++++++++++++----------- src/main.js | 6 +++++- src/utils/hooks/store/useAuthStore.js | 4 ++++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/App.js b/src/App.js index ba68556..463b2e0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,7 @@ -import { RouterProvider } from "react-router-dom"; -import { routers } from "./router"; import useAuthStore from "./utils/hooks/store/useAuthStore"; import { useEffect } from "react"; import { getUsersMe } from "./api/marketApi"; +import { useLocation } from "react-router-dom"; /** * App 컴포넌트는 애플리케이션의 루트 컴포넌트입니다. @@ -11,23 +10,29 @@ import { getUsersMe } from "./api/marketApi"; * @returns JSX.Element - 라우터 설정이 적용된 App 컴포넌트 */ function App() { - const { setAccessToken } = useAuthStore(); + const { setUser, username } = useAuthStore(); + const location = useLocation(); useEffect(() => { - const checkUserStatus = async () => { - try { - const response = await getUsersMe(); - if (response.accessToken) { - setAccessToken(response.accessToken); + const fetchUser = async () => { + if (!username) { + // username이 없을 경우에만 API 호출 + try { + const response = await getUsersMe(); + if (response?.data) { + const { id, username, accessToken } = response.data; + setUser({ id, username, token: accessToken }); + } + } catch (error) { + console.error("Failed to fetch user:", error); } - } catch (error) { - console.error("error:", error); } }; - checkUserStatus(); - }, []); - return ; + fetchUser(); + }, [location, username]); // location과 username을 의존성 배열에 추가 + + return ; } export default App; diff --git a/src/main.js b/src/main.js index b608a22..3cb0f6b 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,8 @@ import ReactDOM from "react-dom/client"; import App from "@/App"; // 스타일시트를 임포트합니다. import "@/styles/index.css"; +import { RouterProvider } from "react-router-dom"; +import { routers } from "./router"; // React Query 클라이언트를 초기화합니다. // 이 클라이언트는 데이터 페칭 및 캐싱을 관리합니다. @@ -24,7 +26,9 @@ enableMocking().finally(() => { // 이를 통해 하위 컴포넌트에서 React Query의 기능을 사용할 수 있습니다. root.render( - + + + ); }); diff --git a/src/utils/hooks/store/useAuthStore.js b/src/utils/hooks/store/useAuthStore.js index 72684ee..ac781f0 100644 --- a/src/utils/hooks/store/useAuthStore.js +++ b/src/utils/hooks/store/useAuthStore.js @@ -10,8 +10,12 @@ const useAuthStore = create( devtools((set) => ({ accessToken: null, isAuthenticated: false, + id: null, + username: null, setAccessToken: (token) => set({ accessToken: token, isAuthenticated: !!token }), + setUser: ({ id, username, token }) => + set({ id, username, accessToken: token, isAuthenticated: !!token }), logout: async () => { try { // 서버의 로그아웃 엔드포인트에 요청 From 48ec6fbc6c5f06b6971c2bffe8115a4d8664897a Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Thu, 1 Feb 2024 17:13:13 +0900 Subject: [PATCH 08/46] =?UTF-8?q?feat:=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 35 +++++--------------- src/main.js | 6 +--- src/utils/hooks/store/useLocationObserver.js | 32 ++++++++++++++++++ 3 files changed, 41 insertions(+), 32 deletions(-) create mode 100644 src/utils/hooks/store/useLocationObserver.js diff --git a/src/App.js b/src/App.js index 463b2e0..246d4ed 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,6 @@ -import useAuthStore from "./utils/hooks/store/useAuthStore"; -import { useEffect } from "react"; -import { getUsersMe } from "./api/marketApi"; -import { useLocation } from "react-router-dom"; +import { RouterProvider } from "react-router-dom"; +import { routers } from "./router"; +import { LocationObserver } from "./utils/hooks/store/useLocationObserver"; /** * App 컴포넌트는 애플리케이션의 루트 컴포넌트입니다. @@ -10,29 +9,11 @@ import { useLocation } from "react-router-dom"; * @returns JSX.Element - 라우터 설정이 적용된 App 컴포넌트 */ function App() { - const { setUser, username } = useAuthStore(); - const location = useLocation(); - - useEffect(() => { - const fetchUser = async () => { - if (!username) { - // username이 없을 경우에만 API 호출 - try { - const response = await getUsersMe(); - if (response?.data) { - const { id, username, accessToken } = response.data; - setUser({ id, username, token: accessToken }); - } - } catch (error) { - console.error("Failed to fetch user:", error); - } - } - }; - - fetchUser(); - }, [location, username]); // location과 username을 의존성 배열에 추가 - - return ; + return ( + + + + ); } export default App; diff --git a/src/main.js b/src/main.js index 3cb0f6b..b608a22 100644 --- a/src/main.js +++ b/src/main.js @@ -5,8 +5,6 @@ import ReactDOM from "react-dom/client"; import App from "@/App"; // 스타일시트를 임포트합니다. import "@/styles/index.css"; -import { RouterProvider } from "react-router-dom"; -import { routers } from "./router"; // React Query 클라이언트를 초기화합니다. // 이 클라이언트는 데이터 페칭 및 캐싱을 관리합니다. @@ -26,9 +24,7 @@ enableMocking().finally(() => { // 이를 통해 하위 컴포넌트에서 React Query의 기능을 사용할 수 있습니다. root.render( - - - + ); }); diff --git a/src/utils/hooks/store/useLocationObserver.js b/src/utils/hooks/store/useLocationObserver.js new file mode 100644 index 0000000..a82b9b5 --- /dev/null +++ b/src/utils/hooks/store/useLocationObserver.js @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; +import useAuthStore from "./useAuthStore"; +import { getUsersMe } from "@/api/marketApi"; + +export function LocationObserver() { + const location = useLocation(); + const { setUser, username } = useAuthStore(); + + useEffect(() => { + console.log("경로 변경 감지:", location.pathname); + // 여기에 사용자 정보를 불러오는 로직을 포함시킬 수 있습니다. + const fetchUser = async () => { + if (!username) { + try { + const response = await getUsersMe(); + if (response?.data) { + const { id, username, accessToken } = response.data; + setUser({ id, username, token: accessToken }); + } + } catch (error) { + console.error("Failed to fetch user:", error); + } + } + }; + + fetchUser(); + }, [location, username, setUser]); + + // 실제 UI를 렌더링할 필요가 없으므로, null을 반환합니다. + return null; +} From 574cb6ef70b2266e01527b306331f8c0cdc9078f Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:17:36 +0900 Subject: [PATCH 09/46] =?UTF-8?q?fix:=20=EC=83=81=ED=92=88=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A7=88=EC=A7=80=EB=A7=89=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B3=84=EC=86=8D=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index f1191cb..7781cbf 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -40,7 +40,7 @@ const ProductList = (props) => { getNextPageParam: ({ curPage, lastPage }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 - return curPage < lastPage ? curPage + 1 : lastPage; + return curPage < lastPage ? curPage + 1 : null; } }); @@ -164,13 +164,13 @@ const ProductListWrap = styled.div` const CardWrap = styled.div` display: flex; flex-wrap: wrap; - justify-content: space-around; + justify-content: flex-start; margin-bottom: 30px; `; const Card = styled.div` width: 180px; - margin-bottom: 10px; + margin: 7px; padding: 10px; box-sizing: border-box; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); From c608e3363d8e0c7fff464b7f04cb172f28155ff5 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 10:51:54 +0900 Subject: [PATCH 10/46] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 8 +---- src/api/marketApi.js | 24 +++++---------- src/components/WithLayout.js | 2 ++ src/utils/LocationObserver.js | 26 ++++++++++++++++ src/utils/hooks/store/useAuthStore.js | 14 ++++++--- src/utils/hooks/store/useLocationObserver.js | 32 -------------------- 6 files changed, 46 insertions(+), 60 deletions(-) create mode 100644 src/utils/LocationObserver.js delete mode 100644 src/utils/hooks/store/useLocationObserver.js diff --git a/src/App.js b/src/App.js index 246d4ed..0b425bb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,5 @@ import { RouterProvider } from "react-router-dom"; import { routers } from "./router"; -import { LocationObserver } from "./utils/hooks/store/useLocationObserver"; - /** * App 컴포넌트는 애플리케이션의 루트 컴포넌트입니다. * RouterProvider를 사용하여 라우터 설정을 적용합니다. @@ -9,11 +7,7 @@ import { LocationObserver } from "./utils/hooks/store/useLocationObserver"; * @returns JSX.Element - 라우터 설정이 적용된 App 컴포넌트 */ function App() { - return ( - - - - ); + return ; } export default App; diff --git a/src/api/marketApi.js b/src/api/marketApi.js index e88c39f..9c06b7a 100644 --- a/src/api/marketApi.js +++ b/src/api/marketApi.js @@ -38,9 +38,14 @@ marketApi.interceptors.response.use( ) { originalRequest._retry = true; try { - const { data } = await marketApi.post("/auth/refresh-token"); - useAuthStore.getState().setAccessToken(data.accessToken); - originalRequest.headers["Authorization"] = `Bearer ${data.accessToken}`; + const { refreshData } = await marketApi.post("/auth/refresh-token"); + useAuthStore.getState().setAccessToken(refreshData.accessToken); + originalRequest.headers["Authorization"] = + `Bearer ${refreshData.accessToken}`; + const { userData } = await marketApi.get("/users/me"); + useAuthStore + .getState() + .setUser({ id: userData.id, userName: userData.username }); return marketApi(originalRequest); } catch (refreshError) { // 리프레시 토큰 요청 실패 시 로그아웃 처리 @@ -88,24 +93,11 @@ export const postAuthLogin = async ({ email, password }) => { useAuthStore.getState().setAccessToken(response.data.accessToken); return response.data; }; - -// 토큰 리프레쉬 (cookie의 refresh토큰 사용 없을 경우 401에러) 삭제예정 -export const postRefreshToken = async () => { - const response = marketApi.post("/auth/refresh-token"); - useAuthStore.getState().setAccessToken(response.data.accessToken); - return await response.data; -}; - // export const getAuthProfile = async () => { return await marketApi.get("/auth/profile", { requiresAuth: true }); }; -//내 정보 가져오기 -export const getUsersMe = async () => { - return await marketApi.get("/users/me", { requiresAuth: true }); -}; - // 유저 정보 업데이트 export const updateUser = async (name) => { return await marketApi.put("/users/update", name, { requiresAuth: true }); diff --git a/src/components/WithLayout.js b/src/components/WithLayout.js index 17cfa71..1897b3b 100644 --- a/src/components/WithLayout.js +++ b/src/components/WithLayout.js @@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom"; import { SuspenseController } from "./SuspenseController"; import Footer from "./footer"; import Header from "./header"; +import { LocationObserver } from "@/utils/LocationObserver"; const DelayedComponent = lazy(() => SuspenseController(import("./SuspenseController"), 3000) @@ -22,6 +23,7 @@ const Modal = lazy(() => import("./popup/Modal")); export default function WithLayout() { return (
+
대기중
}> diff --git a/src/utils/LocationObserver.js b/src/utils/LocationObserver.js new file mode 100644 index 0000000..158a4db --- /dev/null +++ b/src/utils/LocationObserver.js @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import useAuthStore from "./hooks/store/useAuthStore"; +import marketApi from "@/api/marketApi"; + +// 최상단에서 Url 라우팅 시 감지 및 변경 +export function LocationObserver() { + const { setAccessToken, setUser } = useAuthStore(); + + useEffect(() => { + // 첫 기동시 한번 refreshToken 체크 + const fetchUser = async () => { + const { data } = await marketApi.post("/auth/refresh-token"); + if (data?.accessToken) { + setAccessToken(data.accessToken); + const userData = await marketApi.get("/users/me", { + requiresAuth: true + }); + setUser({ id: userData.data?.id, userName: userData.data?.username }); + } + }; + fetchUser(); + }, []); + + // 실제 UI를 렌더링할 필요가 없으므로, null을 반환합니다. + return null; +} diff --git a/src/utils/hooks/store/useAuthStore.js b/src/utils/hooks/store/useAuthStore.js index ac781f0..dd21945 100644 --- a/src/utils/hooks/store/useAuthStore.js +++ b/src/utils/hooks/store/useAuthStore.js @@ -11,11 +11,10 @@ const useAuthStore = create( accessToken: null, isAuthenticated: false, id: null, - username: null, + userName: null, setAccessToken: (token) => - set({ accessToken: token, isAuthenticated: !!token }), - setUser: ({ id, username, token }) => - set({ id, username, accessToken: token, isAuthenticated: !!token }), + set({ accessToken: token, isAuthenticated: true }), + setUser: ({ id, userName }) => set({ id, userName }), logout: async () => { try { // 서버의 로그아웃 엔드포인트에 요청 @@ -24,7 +23,12 @@ const useAuthStore = create( console.error("Logout failed:", error); } // 클라이언트 상태 업데이트 - set({ accessToken: null, isAuthenticated: false }); + set({ + accessToken: null, + isAuthenticated: false, + userName: null, + id: null + }); } })) ); diff --git a/src/utils/hooks/store/useLocationObserver.js b/src/utils/hooks/store/useLocationObserver.js deleted file mode 100644 index a82b9b5..0000000 --- a/src/utils/hooks/store/useLocationObserver.js +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from "react"; -import { useLocation } from "react-router-dom"; -import useAuthStore from "./useAuthStore"; -import { getUsersMe } from "@/api/marketApi"; - -export function LocationObserver() { - const location = useLocation(); - const { setUser, username } = useAuthStore(); - - useEffect(() => { - console.log("경로 변경 감지:", location.pathname); - // 여기에 사용자 정보를 불러오는 로직을 포함시킬 수 있습니다. - const fetchUser = async () => { - if (!username) { - try { - const response = await getUsersMe(); - if (response?.data) { - const { id, username, accessToken } = response.data; - setUser({ id, username, token: accessToken }); - } - } catch (error) { - console.error("Failed to fetch user:", error); - } - } - }; - - fetchUser(); - }, [location, username, setUser]); - - // 실제 UI를 렌더링할 필요가 없으므로, null을 반환합니다. - return null; -} From ae977d7a23db4990e2c84df52b9c63077ea9e6fc Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 11:11:53 +0900 Subject: [PATCH 11/46] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=EB=A5=BC=20=ED=8C=8C=EC=95=85?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/header/components/LogoutMenuList.js | 6 +++--- src/utils/LocationObserver.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index a3bdb5c..5ada242 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -1,7 +1,7 @@ import useAuthStore from "@/utils/hooks/store/useAuthStore"; export const LogoutMenuList = () => { - const { logout } = useAuthStore(); + const { logout, userName } = useAuthStore(); return (
{ className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
  • - Profile - New + HI! {userName} + Name
  • diff --git a/src/utils/LocationObserver.js b/src/utils/LocationObserver.js index 158a4db..9234a02 100644 --- a/src/utils/LocationObserver.js +++ b/src/utils/LocationObserver.js @@ -2,7 +2,7 @@ import { useEffect } from "react"; import useAuthStore from "./hooks/store/useAuthStore"; import marketApi from "@/api/marketApi"; -// 최상단에서 Url 라우팅 시 감지 및 변경 +// 최상단에서 감지하는 함수 변경 export function LocationObserver() { const { setAccessToken, setUser } = useAuthStore(); From 883262ed20164e1a9ecef1dd65b8832c936b0ccc Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 11:22:21 +0900 Subject: [PATCH 12/46] =?UTF-8?q?feat:=20=EB=A1=9C=EC=A7=81=20access?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../header/components/LoginMenuList.js | 8 +++--- .../header/components/LogoutMenuList.js | 11 +++++--- src/utils/LocationObserver.js | 25 +++++++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/header/components/LoginMenuList.js b/src/components/header/components/LoginMenuList.js index 08a2c45..2a7e214 100644 --- a/src/components/header/components/LoginMenuList.js +++ b/src/components/header/components/LoginMenuList.js @@ -25,11 +25,11 @@ export const LoginMenuList = () => {
      -
    • -
      navigate(ROUTES.LOGIN)}>Login
      +
    • navigate(ROUTES.LOGIN)}> +
      Login
    • -
    • -
      navigate(ROUTES.JOIN)}>Join
      +
    • navigate(ROUTES.JOIN)}> +
      Join
  • diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index 5ada242..a9ff80d 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -1,7 +1,10 @@ +import { ROUTES } from "@/utils/constants/routePaths"; import useAuthStore from "@/utils/hooks/store/useAuthStore"; +import { useNavigate } from "react-router-dom"; export const LogoutMenuList = () => { const { logout, userName } = useAuthStore(); + const navigate = useNavigate(); return (
    {
    diff --git a/src/utils/LocationObserver.js b/src/utils/LocationObserver.js index 9234a02..932b00f 100644 --- a/src/utils/LocationObserver.js +++ b/src/utils/LocationObserver.js @@ -4,23 +4,32 @@ import marketApi from "@/api/marketApi"; // 최상단에서 감지하는 함수 변경 export function LocationObserver() { - const { setAccessToken, setUser } = useAuthStore(); + const { setAccessToken, setUser, accessToken } = useAuthStore(); useEffect(() => { - // 첫 기동시 한번 refreshToken 체크 - const fetchUser = async () => { + // 첫 기동시 한번 refreshToken 체크, 이후 accessToken이 변경되면 체크 + const fetchRefresh = async () => { const { data } = await marketApi.post("/auth/refresh-token"); if (data?.accessToken) { setAccessToken(data.accessToken); - const userData = await marketApi.get("/users/me", { - requiresAuth: true - }); - setUser({ id: userData.data?.id, userName: userData.data?.username }); } }; - fetchUser(); + fetchRefresh(); }, []); + useEffect(() => { + // 첫 기동시 한번 refreshToken 체크, 이후 accessToken이 변경되면 체크 + const fetchUser = async () => { + const userData = await marketApi.get("/users/me", { + requiresAuth: true + }); + setUser({ id: userData.data?.id, userName: userData.data?.username }); + }; + if (accessToken) { + fetchUser(); + } + }, [accessToken]); + // 실제 UI를 렌더링할 필요가 없으므로, null을 반환합니다. return null; } From 5f0d4f6e23f3b6bda57c1bfe03536bed79bc5cad Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 12:46:15 +0900 Subject: [PATCH 13/46] =?UTF-8?q?feat:=20authRequire=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../header/components/LogoutMenuList.js | 2 +- src/pages/profile/ProfilePage.js | 3 +++ src/router.js | 22 ++++++++++++++++++- src/utils/constants/routePaths.js | 4 ++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/pages/profile/ProfilePage.js diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index a9ff80d..45b9acb 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -21,7 +21,7 @@ export const LogoutMenuList = () => {
    ); diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index 45b9acb..59139aa 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -1,3 +1,4 @@ +import { getUserMe } from "@/api/marketApi"; import { ROUTES } from "@/utils/constants/routePaths"; import useAuthStore from "@/utils/hooks/store/useAuthStore"; import { useNavigate } from "react-router-dom"; @@ -5,6 +6,9 @@ import { useNavigate } from "react-router-dom"; export const LogoutMenuList = () => { const { logout, userName } = useAuthStore(); const navigate = useNavigate(); + const testFunc = async () => { + const data = await getUserMe(); + }; return (
    {
  • logout()}>
    Logout
  • +
  • +
    test
    +
  • ); From 2a8bfb21fd90820aad55def6449193755ceb11b1 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 14:51:01 +0900 Subject: [PATCH 17/46] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EC=98=AC?= =?UTF-8?q?=EB=A6=B0=20=EC=83=81=ED=92=88=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../header/components/LoginMenuList.js | 8 --- .../header/components/LogoutMenuList.js | 7 --- src/pages/profile/ProfilePage.js | 62 ++++++++++++++++++- src/utils/LocationObserver.js | 6 +- src/utils/hooks/store/useAuthStore.js | 6 +- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/components/header/components/LoginMenuList.js b/src/components/header/components/LoginMenuList.js index 2e6105c..2a7e214 100644 --- a/src/components/header/components/LoginMenuList.js +++ b/src/components/header/components/LoginMenuList.js @@ -1,13 +1,8 @@ -import { getUserMe } from "@/api/marketApi"; import { ROUTES } from "@/utils/constants/routePaths"; import { useNavigate } from "react-router-dom"; export const LoginMenuList = () => { const navigate = useNavigate(); - - const testFunc = async () => { - const data = await getUserMe(); - }; return (
    {
  • navigate(ROUTES.JOIN)}>
    Join
  • -
  • -
    test
    -
  • ); diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index 59139aa..45b9acb 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -1,4 +1,3 @@ -import { getUserMe } from "@/api/marketApi"; import { ROUTES } from "@/utils/constants/routePaths"; import useAuthStore from "@/utils/hooks/store/useAuthStore"; import { useNavigate } from "react-router-dom"; @@ -6,9 +5,6 @@ import { useNavigate } from "react-router-dom"; export const LogoutMenuList = () => { const { logout, userName } = useAuthStore(); const navigate = useNavigate(); - const testFunc = async () => { - const data = await getUserMe(); - }; return (
    {
  • logout()}>
    Logout
  • -
  • -
    test
    -
  • ); diff --git a/src/pages/profile/ProfilePage.js b/src/pages/profile/ProfilePage.js index 669f8d0..dff261e 100644 --- a/src/pages/profile/ProfilePage.js +++ b/src/pages/profile/ProfilePage.js @@ -1,3 +1,63 @@ +import useAuthStore from "@/utils/hooks/store/useAuthStore"; +import ProductList from "../product/components/ProductList"; + export const ProfilePage = () => { - return
    ProfilePage
    ; + const { id, userName, email } = useAuthStore(); + return ( + <> +
    +
    + +
    + +
    +
    +

    Profile

    +

    {id}

    +
    + +
    +
    +
    +

    Sample

    +

    340

    +
    +
    +

    Sample

    +

    $2,004

    +
    +
    +
    +

    {userName}

    +

    {email}

    +
    +
    +
    +
    +
    내가 올린 상품 리스트
    + + + ); }; diff --git a/src/utils/LocationObserver.js b/src/utils/LocationObserver.js index 932b00f..9489a50 100644 --- a/src/utils/LocationObserver.js +++ b/src/utils/LocationObserver.js @@ -23,7 +23,11 @@ export function LocationObserver() { const userData = await marketApi.get("/users/me", { requiresAuth: true }); - setUser({ id: userData.data?.id, userName: userData.data?.username }); + setUser({ + id: userData.data?.id, + userName: userData.data?.username, + email: userData.data?.email + }); }; if (accessToken) { fetchUser(); diff --git a/src/utils/hooks/store/useAuthStore.js b/src/utils/hooks/store/useAuthStore.js index dd21945..54f80dd 100644 --- a/src/utils/hooks/store/useAuthStore.js +++ b/src/utils/hooks/store/useAuthStore.js @@ -12,9 +12,10 @@ const useAuthStore = create( isAuthenticated: false, id: null, userName: null, + email: null, setAccessToken: (token) => set({ accessToken: token, isAuthenticated: true }), - setUser: ({ id, userName }) => set({ id, userName }), + setUser: ({ id, userName, email }) => set({ id, userName, email }), logout: async () => { try { // 서버의 로그아웃 엔드포인트에 요청 @@ -27,7 +28,8 @@ const useAuthStore = create( accessToken: null, isAuthenticated: false, userName: null, - id: null + id: null, + email: null }); } })) From dc8f8cee109eefbe9edb13d0b5ab1e0e3800a6e8 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 15:03:52 +0900 Subject: [PATCH 18/46] =?UTF-8?q?feat:=20ProductList=20=EC=9E=AC=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20props=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductsPage.js | 28 +++++++++++++++++++- src/pages/product/components/ProductList.js | 29 +++------------------ 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index 686edda..e077954 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -1,15 +1,41 @@ +import { getPublishedPosts } from "@/api/marketApi"; import ProductList from "./components/ProductList"; import styled from "@emotion/styled"; +import { useState } from "react"; // 처음 진입하면 보이는 메인 페이지 // api를 호출하여 ProcudtList에 정보를 넘겨서 보여주도록 함 // 스크롤을 통해 더 많은 상품을 동적으로 로드 + export default function ProductsPage() { + const [productList, setProductList] = useState([]); + const keyword = null; + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await getPublishedPosts({ + page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 + limit: 20, + query: keyword ? keyword : "", + orderBy: "createdAt", + direction: "asc" + }); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; return (
    원하시는 상품을 찾아보세요. - +
    ); } diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index f1191cb..79e2a7d 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,4 +1,3 @@ -import { getPublishedPosts } from "@/api/marketApi"; import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; import styled from "@emotion/styled"; @@ -7,32 +6,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 -const ProductList = (props) => { - const { keyword } = props; +const ProductList = ({ getProductList, productList, keyword }) => { const navigate = useNavigate(); - const [productList, setProductList] = useState([]); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 const { openModal, closeModal } = useModalStore(); const observer = useRef(); - const getProductList = async ({ pageParam = 1 }) => { - // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) - const resData = await getPublishedPosts({ - page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 - limit: 20, - query: keyword ? keyword : "", - orderBy: "createdAt", - direction: "asc" - }); - - const { page, lastPage, data: responseData } = resData.data; - setProductList((prevList) => [...prevList, ...responseData]); - - // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 - // page 뜻을 전달하기 위해 이름 curPage로 전달 - return { curPage: page, lastPage }; - }; - const { error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ queryKey: ["productList"], queryFn: getProductList, @@ -40,7 +19,7 @@ const ProductList = (props) => { getNextPageParam: ({ curPage, lastPage }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 - return curPage < lastPage ? curPage + 1 : lastPage; + return curPage < lastPage ? curPage + 1 : null; } }); @@ -164,13 +143,13 @@ const ProductListWrap = styled.div` const CardWrap = styled.div` display: flex; flex-wrap: wrap; - justify-content: space-around; + justify-content: flex-start; margin-bottom: 30px; `; const Card = styled.div` width: 180px; - margin-bottom: 10px; + margin: 7px; padding: 10px; box-sizing: border-box; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); From 5001fb260cc7283378255c1b6087cdbf9382d721 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 15:22:17 +0900 Subject: [PATCH 19/46] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=95=EB=B3=B4=20=ED=91=9C?= =?UTF-8?q?=EA=B8=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/marketApi.js | 19 +++++++++++++++++-- src/pages/product/ProductsPage.js | 1 + src/pages/profile/ProfilePage.js | 28 +++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/api/marketApi.js b/src/api/marketApi.js index e0e8f22..4465cc1 100644 --- a/src/api/marketApi.js +++ b/src/api/marketApi.js @@ -150,8 +150,23 @@ export const getPostById = (id) => { }; // 특정 사용자의 게시물 목록 가져오기 -export const getPostByUser = (userId) => { - return marketApi.get(`/posts/${userId}/posts`); +export const getPostByUser = ({ + page, + limit, + query, + orderBy, + direction, + userId +}) => { + return marketApi.get(`/posts/${userId}/list`, { + params: { + page, + limit, + query, + orderBy, + direction + } + }); }; export default marketApi; diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index e077954..76812d8 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -27,6 +27,7 @@ export default function ProductsPage() { // page 뜻을 전달하기 위해 이름 curPage로 전달 return { curPage: page, lastPage }; }; + return (
    원하시는 상품을 찾아보세요. diff --git a/src/pages/profile/ProfilePage.js b/src/pages/profile/ProfilePage.js index dff261e..97c7699 100644 --- a/src/pages/profile/ProfilePage.js +++ b/src/pages/profile/ProfilePage.js @@ -1,8 +1,30 @@ import useAuthStore from "@/utils/hooks/store/useAuthStore"; import ProductList from "../product/components/ProductList"; +import { useState } from "react"; +import { getPostByUser } from "@/api/marketApi"; export const ProfilePage = () => { const { id, userName, email } = useAuthStore(); + const [productList, setProductList] = useState([]); + const keyword = null; + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await getPostByUser({ + page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 + limit: 20, + query: keyword ? keyword : "", + orderBy: "createdAt", + direction: "asc", + userId: id + }); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; return ( <>
    @@ -57,7 +79,11 @@ export const ProfilePage = () => {
    내가 올린 상품 리스트
    - + ); }; From 6e311d1c6274b96230cb2d635f1f96f71a6b0779 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 15:44:25 +0900 Subject: [PATCH 20/46] =?UTF-8?q?feat:=20queryKey=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductsPage.js | 1 + src/pages/product/components/ProductList.js | 4 ++-- src/pages/profile/ProfilePage.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index 76812d8..f95d309 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -36,6 +36,7 @@ export default function ProductsPage() { getProductList={getProductList} productList={productList} keyword={keyword} + queryKey={["HomeProductList"]} />
    ); diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 79e2a7d..917eb24 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -6,14 +6,14 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 -const ProductList = ({ getProductList, productList, keyword }) => { +const ProductList = ({ getProductList, productList, keyword, queryKey }) => { const navigate = useNavigate(); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 const { openModal, closeModal } = useModalStore(); const observer = useRef(); const { error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ - queryKey: ["productList"], + queryKey: queryKey, queryFn: getProductList, // 위의 getPublishedPosts 결과값으로 얻은 page (현재 받아온 페이지) , lastpage (총 페이지) getNextPageParam: ({ curPage, lastPage }) => { diff --git a/src/pages/profile/ProfilePage.js b/src/pages/profile/ProfilePage.js index 97c7699..2461f09 100644 --- a/src/pages/profile/ProfilePage.js +++ b/src/pages/profile/ProfilePage.js @@ -83,6 +83,7 @@ export const ProfilePage = () => { getProductList={getProductList} productList={productList} keyword={keyword} + queryKey={["ProfileProductList"]} /> ); From e4d5b124ea4e94909db2b49a3de953a75ed8d70f Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:50:04 +0900 Subject: [PATCH 21/46] =?UTF-8?q?refactor:=20productList=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B2=B0=EA=B3=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20api=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductSearchResultPage.js | 28 +++++++++++++++++++- src/pages/product/ProductsSearchPage.js | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/pages/product/ProductSearchResultPage.js b/src/pages/product/ProductSearchResultPage.js index 851262b..9e804e7 100644 --- a/src/pages/product/ProductSearchResultPage.js +++ b/src/pages/product/ProductSearchResultPage.js @@ -3,9 +3,30 @@ import styled from "@emotion/styled"; import { useParams } from "react-router-dom"; import ProductList from "./components/ProductList"; +import { getPublishedPosts } from "@/api/marketApi"; +import { useState } from "react"; const ProductSearchResultPage = () => { const { keyword } = useParams(); // 검색어 + const [productList, setProductList] = useState([]); + + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await getPublishedPosts({ + page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 + limit: 20, + query: keyword ? keyword : "", + orderBy: "createdAt", + direction: "asc" + }); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; return (
    @@ -14,7 +35,12 @@ const ProductSearchResultPage = () => { 결과입니다. - +
    ); }; diff --git a/src/pages/product/ProductsSearchPage.js b/src/pages/product/ProductsSearchPage.js index e539875..86feee5 100644 --- a/src/pages/product/ProductsSearchPage.js +++ b/src/pages/product/ProductsSearchPage.js @@ -26,7 +26,7 @@ const ProductsSearchPage = () => { const handleSearchSubmit = (current) => { let searchWord; - console.log("type::", typeof current); + // console.log("type::", typeof current); if (typeof current === "string") { // 최근 검색어를 클릭한 경우 searchWord = current.trim(); From c635184480d54dec0759e3f77a6349112c770498 Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:56:57 +0900 Subject: [PATCH 22/46] =?UTF-8?q?refactor:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20?= =?UTF-8?q?token=20->=20userId=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductsSearchPage.js | 6 +- .../product/components/RecentSearches.js | 8 +-- .../product/components/SaveSearchStorage.js | 68 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/pages/product/ProductsSearchPage.js b/src/pages/product/ProductsSearchPage.js index 86feee5..0513a44 100644 --- a/src/pages/product/ProductsSearchPage.js +++ b/src/pages/product/ProductsSearchPage.js @@ -9,7 +9,7 @@ import { const ProductsSearchPage = () => { const navigate = useNavigate(); - const { isAuthenticated, accessToken } = useAuthStore(); + const { isAuthenticated, id: loginId } = useAuthStore(); const [inputVal, setInputVal] = useState(""); const [searchTerm, setSearchTerm] = useState(""); @@ -17,9 +17,9 @@ const ProductsSearchPage = () => { // 최근 검색어 저장 로직 추가 // 로컬 스토리지 사용할 수 없을 때 브라우저 캐시 사용 try { - saveLocalStorage(word, isAuthenticated, accessToken); + saveLocalStorage(word, isAuthenticated, loginId); } catch (error) { - saveCacheStorage(word, isAuthenticated, accessToken); + saveCacheStorage(word, isAuthenticated, loginId); } }; diff --git a/src/pages/product/components/RecentSearches.js b/src/pages/product/components/RecentSearches.js index 3d410b0..dd6085c 100644 --- a/src/pages/product/components/RecentSearches.js +++ b/src/pages/product/components/RecentSearches.js @@ -6,7 +6,7 @@ import { getCacheStorage } from "@/pages/product/components/SaveSearchStorage"; // 검색 - 최근 검색어 목록 컴포넌트 const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { - const { isAuthenticated, accessToken } = useAuthStore(); + const { isAuthenticated, id: loginId } = useAuthStore(); const [recentSearches, setRecentSearches] = useState([]); useEffect(() => { @@ -14,13 +14,13 @@ const RecentSearches = ({ searchTerm, handleSearchSubmit }) => { try { // 로컬 스토리지 사용할 수 없을 때 브라우저 캐시 사용 - storedSearches = getLocalStorage(isAuthenticated, accessToken); + storedSearches = getLocalStorage(isAuthenticated, loginId); } catch (error) { - storedSearches = getCacheStorage(isAuthenticated, accessToken); + storedSearches = getCacheStorage(isAuthenticated, loginId); } setRecentSearches(storedSearches); - }, [searchTerm, isAuthenticated, accessToken]); + }, [searchTerm, isAuthenticated, loginId]); return ( <> diff --git a/src/pages/product/components/SaveSearchStorage.js b/src/pages/product/components/SaveSearchStorage.js index f5a99c1..a5e1006 100644 --- a/src/pages/product/components/SaveSearchStorage.js +++ b/src/pages/product/components/SaveSearchStorage.js @@ -16,7 +16,7 @@ export const cacheStorage = { }; // 로컬 스토리지에 최근 검색어 저장 -export const saveLocalStorage = (word, isAuthenticated, accessToken) => { +export const saveLocalStorage = (word, isAuthenticated, loginId) => { let updatedSearches; // 로컬 스토리지에서 최근 검색어 가져옴 @@ -24,17 +24,17 @@ export const saveLocalStorage = (word, isAuthenticated, accessToken) => { JSON.parse(localStorage.getItem("RecentSearches")) || []; if (isAuthenticated) { - // 로그인 되어있는 경우, 저장된 스토리지의 토큰과 로그인한 유저의 토큰 값 비교 - const existingTokenIndex = existingSearches.findIndex( - (item) => item.token === accessToken + // 로그인 되어있는 경우, 저장된 스토리지의 id와 로그인한 유저의 id 값 비교 + const existingIdIndex = existingSearches.findIndex( + (item) => item.userId === loginId ); - if (existingTokenIndex !== -1) { - // 이미 해당 토큰이 존재하는 경우, 해당 토큰의 검색어 업데이트 + if (existingIdIndex !== -1) { + // 이미 해당 id가 존재하는 경우, 해당 id의 검색어 업데이트 updatedSearches = existingSearches.map((item, index) => - index === existingTokenIndex + index === existingIdIndex ? { - token: accessToken, + userId: loginId, // 새로운 검색어를 추가하고 중복 제거 후 최대 4개까지 유지 keyword: [word, ...item.keyword] .filter((keyword, i, self) => self.indexOf(keyword) === i) @@ -43,10 +43,10 @@ export const saveLocalStorage = (word, isAuthenticated, accessToken) => { : item ); } else { - // 해당 토큰이 존재하지 않는 경우, 새로운 토큰과 검색어를 추가 + // 해당 id가 존재하지 않는 경우, 새로운 userId와 검색어를 추가 updatedSearches = [ ...existingSearches, - { token: accessToken, keyword: [word] } + { userId: loginId, keyword: [word] } ]; } } else { @@ -57,42 +57,42 @@ export const saveLocalStorage = (word, isAuthenticated, accessToken) => { }; // 로컬 스토리지에 저장된 최근 검색어 조회 -export const getLocalStorage = (isAuthenticated, accessToken) => { +export const getLocalStorage = (isAuthenticated, loginId) => { let storedSearches = []; - if (isAuthenticated && accessToken) { - // 로그인 및 유효한 토큰을 가진 사용자인 경우 + if (isAuthenticated && loginId) { + // 로그인 및 유효한 id를 가진 사용자인 경우 // 로컬 스토리지에서 "RecentSearches" 키의 값을 가져와 파싱 - const userTokenSearches = + const userIdSearches = (JSON.parse(localStorage.getItem("RecentSearches")) || [])[0] || {}; - // 해당 토큰의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 - storedSearches = userTokenSearches.keyword || []; + // 해당 유저 id의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 + storedSearches = userIdSearches.keyword || []; } - // 최종적으로 로그인 상태가 아니거나 토큰이 유효하지 않은 경우 빈 배열 반환 + // 최종적으로 로그인 상태가 아니거나 id가 유효하지 않은 경우 빈 배열 반환 return storedSearches; }; // 캐시 스토리지에 최근 검색어 저장 -export const saveCacheStorage = (word, isAuthenticated, accessToken) => { +export const saveCacheStorage = (word, isAuthenticated, loginId) => { let updatedSearches; const existingSearches = JSON.parse(cacheStorage.getItem("RecentSearches")) || []; if (isAuthenticated) { - // 로그인 되어있는 경우, 저장된 스토리지의 토큰과 로그인한 유저의 토큰 값 비교 - const existingTokenIndex = existingSearches.findIndex( - (item) => item.token === accessToken + // 로그인 되어있는 경우, 저장된 스토리지의 id와 로그인한 유저의 id 값 비교 + const existingIdIndex = existingSearches.findIndex( + (item) => item.userId === loginId ); - if (existingTokenIndex !== -1) { - // 이미 해당 토큰이 존재하는 경우, 해당 토큰의 검색어 업데이트 + if (existingIdIndex !== -1) { + // 이미 해당 id가 존재하는 경우, 해당 id의 검색어 업데이트 updatedSearches = existingSearches.map((item, index) => - index === existingTokenIndex + index === existingIdIndex ? { - token: accessToken, + userId: loginId, // 새로운 검색어를 추가하고 중복 제거 후 최대 4개까지 유지 keyword: [word, ...item.keyword] .filter((keyword, i, self) => self.indexOf(keyword) === i) @@ -101,10 +101,10 @@ export const saveCacheStorage = (word, isAuthenticated, accessToken) => { : item ); } else { - // 해당 토큰이 존재하지 않는 경우, 새로운 토큰과 검색어를 추가 + // 해당 id가 존재하지 않는 경우, 새로운 userId와 검색어를 추가 updatedSearches = [ ...existingSearches, - { token: accessToken, keyword: [word] } + { userId: loginId, keyword: [word] } ]; } } else { @@ -115,20 +115,20 @@ export const saveCacheStorage = (word, isAuthenticated, accessToken) => { }; // 캐시 스토리지에 저장된 최근 검색어 조회 -export const getCacheStorage = (isAuthenticated, accessToken) => { +export const getCacheStorage = (isAuthenticated, loginId) => { let storedSearches = []; - if (isAuthenticated && accessToken) { - // 로그인 및 유효한 토큰을 가진 사용자인 경우 + if (isAuthenticated && loginId) { + // 로그인 및 유효한 id 가진 사용자인 경우 // 캐시 스토리지에서 "RecentSearches" 키의 값을 가져와 파싱 - const userTokenSearches = + const userIdSearches = (JSON.parse(cacheStorage.getItem("RecentSearches")) || [])[0] || {}; - // 해당 토큰의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 - storedSearches = userTokenSearches.keyword || []; + // 해당 id의 최근 검색어를 가져오거나, 값이 없으면 빈 배열 사용 + storedSearches = userIdSearches.keyword || []; } - // 최종적으로 로그인 상태가 아니거나 토큰이 유효하지 않은 경우 빈 배열 반환 + // 최종적으로 로그인 상태가 아니거나 id가 유효하지 않은 경우 빈 배열 반환 return storedSearches; }; From 0cf7ca34d5ae7917dd8ec2c42c554611a88c3e28 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Fri, 2 Feb 2024 16:39:19 +0900 Subject: [PATCH 23/46] =?UTF-8?q?feat:=20queryKey=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductSearchResultPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/product/ProductSearchResultPage.js b/src/pages/product/ProductSearchResultPage.js index 9e804e7..1a7d2a9 100644 --- a/src/pages/product/ProductSearchResultPage.js +++ b/src/pages/product/ProductSearchResultPage.js @@ -39,7 +39,7 @@ const ProductSearchResultPage = () => { getProductList={getProductList} productList={productList} keyword={keyword} - queryKey={["HomeProductList"]} + queryKey={["SearchProductList"]} />
    ); From 28fc6fde64aa416d690cd454c5b33953b0454671 Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:28:00 +0900 Subject: [PATCH 24/46] =?UTF-8?q?feat:=20productList=20=EC=B2=AB=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=EC=8B=9C=EC=97=90=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A4=84=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 112 +++++++++++++++++--- 1 file changed, 95 insertions(+), 17 deletions(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 917eb24..ec9f932 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,27 +1,30 @@ import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; import styled from "@emotion/styled"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { css } from "@emotion/react"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 const ProductList = ({ getProductList, productList, keyword, queryKey }) => { + const queryClient = useQueryClient(); const navigate = useNavigate(); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 const { openModal, closeModal } = useModalStore(); const observer = useRef(); - const { error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ - queryKey: queryKey, - queryFn: getProductList, - // 위의 getPublishedPosts 결과값으로 얻은 page (현재 받아온 페이지) , lastpage (총 페이지) - getNextPageParam: ({ curPage, lastPage }) => { - // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 - // 여기서 return 하는 값은 pageParam으로 전달 됨 - return curPage < lastPage ? curPage + 1 : null; - } - }); + const { error, fetchNextPage, hasNextPage, isFetching, isLoading } = + useInfiniteQuery({ + queryKey: queryKey, + queryFn: getProductList, + getNextPageParam: ({ curPage, lastPage }) => { + // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 + // 여기서 return 하는 값은 pageParam으로 전달 됨 + return curPage < lastPage ? curPage + 1 : null; + ㅉ; + } + }); const observerOption = { root: null, @@ -48,6 +51,7 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { observer.current.observe(node); } }, + [fetchNextPage, hasNextPage] ); @@ -59,6 +63,16 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { }); }; + // 스켈레톤 카드 컴포넌트 + const SkeletonCard = () => ( + + + + + + + ); + useEffect(() => { // 초기 데이터 로딩 @@ -67,13 +81,23 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { const isTop = window.scrollY === 0; setShowTopButton(!isTop); }; + if (!isLoading) { + window.addEventListener("scroll", handleTop); - window.addEventListener("scroll", handleTop); + return () => { + // 스크롤 이벤트 리스너 제거 + window.removeEventListener("scroll", handleTop); + }; + } + }, [keyword]); + useEffect(() => { return () => { - window.removeEventListener("scroll", handleTop); + // 컴포넌트가 언마운트될 때 React Query의 쿼리를 초기화 + // queryClient.cancelQueries(queryKey); + queryClient.removeQueries(queryKey); }; - }, [keyword]); + }, []); useEffect(() => { // 추후에 조건 풀 지 확인해보기 @@ -97,14 +121,25 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { return (
    - {isFetching && } + {isLoading && ( + + {[...Array(6)].map((_, index) => ( + + ))} + + )} {productList && productList.length > 0 ? ( <> + {!isLoading && isFetching && } {productList.map((product, index) => ( navigate(`/web/product/${product.id}`)}> {product.location} @@ -116,7 +151,7 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { {} : scrollToTop}> + onClick={isFetching || isLoading ? () => {} : scrollToTop}> TOP @@ -145,6 +180,12 @@ const CardWrap = styled.div` flex-wrap: wrap; justify-content: flex-start; margin-bottom: 30px; + + ${(props) => + props.isLoading && + css` + filter: grayscale(100%); + `} `; const Card = styled.div` @@ -153,6 +194,14 @@ const Card = styled.div` padding: 10px; box-sizing: border-box; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); + + ${(props) => + props.isLoading && + css` + opacity: 0.5; + filter: grayscale(100%); + backgroundcolor: "#f0f0f0"; + `} `; const CardImg = styled.img` @@ -161,6 +210,12 @@ const CardImg = styled.img` background-size: cover; border-radius: 12px; margin-bottom: 5px; + + ${(props) => + props.isLoading && + css` + background-color: rgba(0, 0, 0, 0.2); + `} `; const ProductBadge = styled.div` @@ -174,16 +229,39 @@ const ProductBadge = styled.div` justify-content: center; border-radius: 8px; margin: 10px 0 0 0; + + ${(props) => + props.isLoading && + css` + width: 50px; + background-color: rgba(0, 0, 0, 0.2); + `} `; const ProductPrice = styled.div` font-size: 14px; + + ${(props) => + props.isLoading && + css` + width: 50px; + height: 20px; + background-color: rgba(0, 0, 0, 0.2); + `} `; const ProductTitle = styled.h1` font-size: 16px; font-weight: 700; margin: 5px 0; + + ${(props) => + props.isLoading && + css` + width: 70px; + height: 20px; + background-color: rgba(0, 0, 0, 0.2); + `} `; const NoticeMsg = styled.p` From 60e3d890dc43727286d6713a25ae7e5f402aa7c2 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Mon, 5 Feb 2024 22:42:17 +0900 Subject: [PATCH 25/46] =?UTF-8?q?fix=20:=20productDetailPage=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductDetailPage.js | 30 +++++++------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index 392bd84..029fc01 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -1,7 +1,7 @@ +import { getPostById } from "@/api/marketApi"; import ProductImageCarousel from "@/components/carousel/ProductImageCarousel"; -import Dialog from "@/components/popup/Dialog"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import { useState } from "react"; +import { useEffect } from "react"; const images = [ "https://i.pinimg.com/550x/a9/f1/2a/a9f12ad9bfe0baa4f6e629d1e0fa439c.jpg", @@ -10,25 +10,15 @@ const images = [ ]; export default function ProductDetailPage() { - // console.log(getPublishedPosts().then((e) => console.log(e))); - const { openModal, closeModal } = useModalStore(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const openDialog = () => setIsDialogOpen(true); - const closeDialog = () => setIsDialogOpen(false); + const { openModal } = useModalStore(); + useEffect(() => { + console.log(getPostById(1).then((e) => console.log(e))); + }, []); const openCustomPopup = ({ process }) => { const customContent = (
    -
    -

    - 가입에 {process ? "성공" : "실패"} 했습니다. -

    - {process ? ( -

    - 확인 버튼 클릭으로 로그인 페이지로 이동합니다. -

    - ) : ( -

    다시 시도해 주시기 바랍니다.

    - )} +
    +

    장바구니에 저장하였습니다.

    @@ -40,10 +30,6 @@ export default function ProductDetailPage() { return (
    - - -
    모달 컨텐츠
    -
    From ce8d7ec75d3bbb47dffcefad1f857a8f0c0a968e Mon Sep 17 00:00:00 2001 From: wowyj26 <152588718+wowyj26@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:03:12 +0900 Subject: [PATCH 26/46] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EA=B0=84=20=EC=98=A4=ED=83=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index ec9f932..414f5a2 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -22,7 +22,6 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 return curPage < lastPage ? curPage + 1 : null; - ㅉ; } }); From a66ee1a4a17a06d2b368ec51f0e7889ab736818f Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:57:06 +0900 Subject: [PATCH 27/46] =?UTF-8?q?fix=20:=20=EC=9E=84=EC=8B=9C=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/carousel/ImageSwiperSlide.js | 1 + src/pages/product/ProductDetailPage.js | 28 ++++++++++++----- src/pages/product/ProductsPage.js | 35 +++++---------------- src/pages/product/components/ProductList.js | 22 ++++++++++--- src/router.js | 7 ++--- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/components/carousel/ImageSwiperSlide.js b/src/components/carousel/ImageSwiperSlide.js index 506d3f3..fe2114b 100644 --- a/src/components/carousel/ImageSwiperSlide.js +++ b/src/components/carousel/ImageSwiperSlide.js @@ -27,6 +27,7 @@ export default ImageSwiperSlide; const containerStyle = css` width: 100%; // 컨테이너 너비를 100%로 설정 padding-top: 56.25%; // 16:9 비율의 높이를 설정 + min-height: 200px; // 이미지 높이를 100%로 설정 position: relative; // 자식 요소를 절대 위치로 배치하기 위한 상대 위치 설정 `; diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index 029fc01..54defdd 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -1,7 +1,8 @@ import { getPostById } from "@/api/marketApi"; import ProductImageCarousel from "@/components/carousel/ProductImageCarousel"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; const images = [ "https://i.pinimg.com/550x/a9/f1/2a/a9f12ad9bfe0baa4f6e629d1e0fa439c.jpg", @@ -11,9 +12,20 @@ const images = [ export default function ProductDetailPage() { const { openModal } = useModalStore(); + const { productid } = useParams(); + const [product, setProduct] = useState({}); + + console.log(productid, "id"); useEffect(() => { - console.log(getPostById(1).then((e) => console.log(e))); + getPostById(productid).then((e) => { + console.log(e.data); + setProduct(e.data); + }); }, []); + const { title, price, content, imgUrls = [], location } = product; + + console.log(imgUrls, "imgUrls"); + const openCustomPopup = ({ process }) => { const customContent = (
    @@ -29,23 +41,23 @@ export default function ProductDetailPage() { }; return (
    - +
    -
    WINIA Air Washer
    -
    Used - Like new
    +
    {title}
    +
    {location}
    -
    30,000₩
    +
    {price}₩
    -

    me.

    +

    {content}

    diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index f95d309..cc9ef00 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -1,41 +1,22 @@ -import { getPublishedPosts } from "@/api/marketApi"; -import ProductList from "./components/ProductList"; import styled from "@emotion/styled"; -import { useState } from "react"; +import ProductList from "./components/ProductList"; // 처음 진입하면 보이는 메인 페이지 // api를 호출하여 ProcudtList에 정보를 넘겨서 보여주도록 함 // 스크롤을 통해 더 많은 상품을 동적으로 로드 export default function ProductsPage() { - const [productList, setProductList] = useState([]); - const keyword = null; - const getProductList = async ({ pageParam = 1 }) => { - // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) - const resData = await getPublishedPosts({ - page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 - limit: 20, - query: keyword ? keyword : "", - orderBy: "createdAt", - direction: "asc" - }); - - const { page, lastPage, data: responseData } = resData.data; - setProductList((prevList) => [...prevList, ...responseData]); - - // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 - // page 뜻을 전달하기 위해 이름 curPage로 전달 - return { curPage: page, lastPage }; - }; - return (
    원하시는 상품을 찾아보세요. -
    diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index ec9f932..656a64e 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,13 +1,26 @@ +import { getPublishedPosts } from "@/api/marketApi"; import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import styled from "@emotion/styled"; import { css } from "@emotion/react"; +import styled from "@emotion/styled"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 -const ProductList = ({ getProductList, productList, keyword, queryKey }) => { +const ProductList = ({ param, queryKey }) => { + const [productList, setProductList] = useState([]); + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await getPublishedPosts(param); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; const queryClient = useQueryClient(); const navigate = useNavigate(); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 @@ -22,7 +35,6 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 return curPage < lastPage ? curPage + 1 : null; - ㅉ; } }); @@ -89,7 +101,7 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { window.removeEventListener("scroll", handleTop); }; } - }, [keyword]); + }, [param.keyword]); useEffect(() => { return () => { @@ -159,7 +171,7 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => {
    {!productList?.length && !isFetching && ( - {`${keyword ? "검색 결과가" : "상품이"} 없습니다.`} + {`${param.keyword ? "검색 결과가" : "상품이"} 없습니다.`} )}
    diff --git a/src/router.js b/src/router.js index 1c15d2a..997d18f 100644 --- a/src/router.js +++ b/src/router.js @@ -3,14 +3,13 @@ import Header from "@/components/header"; import NotFoundPage from "@/pages/NotFoundPage"; import ProductsPage from "@/pages/product/ProductsPage"; import { ROUTES } from "@/utils/constants/routePaths"; -import { createBrowserRouter } from "react-router-dom"; +import { Navigate, createBrowserRouter } from "react-router-dom"; import { JoinPage } from "./pages/join/JoinPage"; import { LoginPage } from "./pages/login/LoginPage"; import ProductDetailPage from "./pages/product/ProductDetailPage"; -import ProductsSearchPage from "./pages/product/ProductsSearchPage"; import ProductSearchResultPage from "./pages/product/ProductSearchResultPage"; +import ProductsSearchPage from "./pages/product/ProductsSearchPage"; import { ProfilePage } from "./pages/profile/ProfilePage"; -import { Navigate } from "react-router-dom"; import useAuthStore from "./utils/hooks/store/useAuthStore"; // 코드 스플리팅을 위해 React.lazy를 사용하는 주석 처리된 예시입니다. @@ -24,7 +23,7 @@ export const routeConfig = [ // 홈 페이지 경로, ProductPage 컴포넌트를 렌더링합니다. // `index: true`는 이 라우트가 자식 라우트보다 우선순위가 높음을 나타냅니다. { path: ROUTES.HOME, element: , index: true }, - { path: ROUTES.PRODUCT, element: }, + { path: ROUTES.PRODUCT, element: , authRequire: false }, { path: ROUTES.SEARCH, element: }, { path: ROUTES.SEARCH_RESULT, element: }, { path: ROUTES.LOGIN, element: }, From 881cda76877a8eb5d7dd8476fd79c6b504cb897e Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:59:14 +0900 Subject: [PATCH 28/46] =?UTF-8?q?Revert=20"fix=20:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a66ee1a4a17a06d2b368ec51f0e7889ab736818f. --- src/components/carousel/ImageSwiperSlide.js | 1 - src/pages/product/ProductsPage.js | 35 ++++++++++++++++----- src/pages/product/components/ProductList.js | 22 +++---------- src/router.js | 7 +++-- 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/components/carousel/ImageSwiperSlide.js b/src/components/carousel/ImageSwiperSlide.js index fe2114b..506d3f3 100644 --- a/src/components/carousel/ImageSwiperSlide.js +++ b/src/components/carousel/ImageSwiperSlide.js @@ -27,7 +27,6 @@ export default ImageSwiperSlide; const containerStyle = css` width: 100%; // 컨테이너 너비를 100%로 설정 padding-top: 56.25%; // 16:9 비율의 높이를 설정 - min-height: 200px; // 이미지 높이를 100%로 설정 position: relative; // 자식 요소를 절대 위치로 배치하기 위한 상대 위치 설정 `; diff --git a/src/pages/product/ProductsPage.js b/src/pages/product/ProductsPage.js index cc9ef00..f95d309 100644 --- a/src/pages/product/ProductsPage.js +++ b/src/pages/product/ProductsPage.js @@ -1,22 +1,41 @@ -import styled from "@emotion/styled"; +import { getPublishedPosts } from "@/api/marketApi"; import ProductList from "./components/ProductList"; +import styled from "@emotion/styled"; +import { useState } from "react"; // 처음 진입하면 보이는 메인 페이지 // api를 호출하여 ProcudtList에 정보를 넘겨서 보여주도록 함 // 스크롤을 통해 더 많은 상품을 동적으로 로드 export default function ProductsPage() { + const [productList, setProductList] = useState([]); + const keyword = null; + const getProductList = async ({ pageParam = 1 }) => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await getPublishedPosts({ + page: pageParam !== 1 ? pageParam : 1, // 1 페이지가 아니면 nextPage(현재+1 된 값)을 호출 + limit: 20, + query: keyword ? keyword : "", + orderBy: "createdAt", + direction: "asc" + }); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; + return (
    원하시는 상품을 찾아보세요. +
    diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 656a64e..ec9f932 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,26 +1,13 @@ -import { getPublishedPosts } from "@/api/marketApi"; import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import { css } from "@emotion/react"; import styled from "@emotion/styled"; +import { css } from "@emotion/react"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; // 상품 리스트를 보여주는 공용 컴포넌트 -const ProductList = ({ param, queryKey }) => { - const [productList, setProductList] = useState([]); - const getProductList = async ({ pageParam = 1 }) => { - // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) - const resData = await getPublishedPosts(param); - - const { page, lastPage, data: responseData } = resData.data; - setProductList((prevList) => [...prevList, ...responseData]); - - // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 - // page 뜻을 전달하기 위해 이름 curPage로 전달 - return { curPage: page, lastPage }; - }; +const ProductList = ({ getProductList, productList, keyword, queryKey }) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const [showTopButton, setShowTopButton] = useState(false); // "맨 위로 가기" 버튼의 표시 여부를 제어하는 상태 @@ -35,6 +22,7 @@ const ProductList = ({ param, queryKey }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 return curPage < lastPage ? curPage + 1 : null; + ㅉ; } }); @@ -101,7 +89,7 @@ const ProductList = ({ param, queryKey }) => { window.removeEventListener("scroll", handleTop); }; } - }, [param.keyword]); + }, [keyword]); useEffect(() => { return () => { @@ -171,7 +159,7 @@ const ProductList = ({ param, queryKey }) => {
    {!productList?.length && !isFetching && ( - {`${param.keyword ? "검색 결과가" : "상품이"} 없습니다.`} + {`${keyword ? "검색 결과가" : "상품이"} 없습니다.`} )}
    diff --git a/src/router.js b/src/router.js index 997d18f..1c15d2a 100644 --- a/src/router.js +++ b/src/router.js @@ -3,13 +3,14 @@ import Header from "@/components/header"; import NotFoundPage from "@/pages/NotFoundPage"; import ProductsPage from "@/pages/product/ProductsPage"; import { ROUTES } from "@/utils/constants/routePaths"; -import { Navigate, createBrowserRouter } from "react-router-dom"; +import { createBrowserRouter } from "react-router-dom"; import { JoinPage } from "./pages/join/JoinPage"; import { LoginPage } from "./pages/login/LoginPage"; import ProductDetailPage from "./pages/product/ProductDetailPage"; -import ProductSearchResultPage from "./pages/product/ProductSearchResultPage"; import ProductsSearchPage from "./pages/product/ProductsSearchPage"; +import ProductSearchResultPage from "./pages/product/ProductSearchResultPage"; import { ProfilePage } from "./pages/profile/ProfilePage"; +import { Navigate } from "react-router-dom"; import useAuthStore from "./utils/hooks/store/useAuthStore"; // 코드 스플리팅을 위해 React.lazy를 사용하는 주석 처리된 예시입니다. @@ -23,7 +24,7 @@ export const routeConfig = [ // 홈 페이지 경로, ProductPage 컴포넌트를 렌더링합니다. // `index: true`는 이 라우트가 자식 라우트보다 우선순위가 높음을 나타냅니다. { path: ROUTES.HOME, element: , index: true }, - { path: ROUTES.PRODUCT, element: , authRequire: false }, + { path: ROUTES.PRODUCT, element: }, { path: ROUTES.SEARCH, element: }, { path: ROUTES.SEARCH_RESULT, element: }, { path: ROUTES.LOGIN, element: }, From 07be6cc5537e1614e3e487f9ec7f8fb6f77c5b67 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:00:02 +0900 Subject: [PATCH 29/46] =?UTF-8?q?fix=20:=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index ec9f932..b6eb3cd 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -1,7 +1,7 @@ import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import styled from "@emotion/styled"; import { css } from "@emotion/react"; +import styled from "@emotion/styled"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -22,7 +22,6 @@ const ProductList = ({ getProductList, productList, keyword, queryKey }) => { // 마지막 페이지인 경우에는 더 이상 호출 불필요 , 마지막 페이지보다 전이면 +1 해준다 // 여기서 return 하는 값은 pageParam으로 전달 됨 return curPage < lastPage ? curPage + 1 : null; - ㅉ; } }); From 6b356da8e053868235255b542d3c3161242c8343 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:08:59 +0900 Subject: [PATCH 30/46] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductDetailPage.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index 54defdd..cd9a9b8 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -1,14 +1,9 @@ -import { getPostById } from "@/api/marketApi"; +import { getPostById, getPublishedPosts } from "@/api/marketApi"; import ProductImageCarousel from "@/components/carousel/ProductImageCarousel"; import useModalStore from "@/utils/hooks/store/useModalStore"; import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; - -const images = [ - "https://i.pinimg.com/550x/a9/f1/2a/a9f12ad9bfe0baa4f6e629d1e0fa439c.jpg", - "https://species.nibr.go.kr/UPLOAD/digital/species/12000009/120000095823/BIMGMM0000386036_20221116112438319509.jpg", - "https://mblogthumb-phinf.pstatic.net/MjAxODAyMDJfMTcx/MDAxNTE3NTUxOTIxNDcz.4p7O7MZoKYKwd9FSAVZBdJQEayDerw9nzUCPKNzfSL4g.Cx4zwz5E5GiYeUS1EGelbBU4Z2gPj9jn0ZjCQxD55gsg.JPEG.phjphk12/image_3575628891517551874701.jpg?type=w800" -]; +import ProductsListComponent from "./components/ProductListComponent"; export default function ProductDetailPage() { const { openModal } = useModalStore(); From 352510fc77375448523371941d1554add61e87a8 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:09:26 +0900 Subject: [PATCH 31/46] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=97=90=20=EC=83=81=ED=92=88=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/ProductDetailPage.js | 58 ++++--------------- .../components/ProductListComponent.js | 49 ++++++++++++++++ 2 files changed, 61 insertions(+), 46 deletions(-) create mode 100644 src/pages/product/components/ProductListComponent.js diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index cd9a9b8..49516d4 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -55,52 +55,18 @@ export default function ProductDetailPage() { 장바구니에 넣기
    -
    -
    Similar items
    -
    -
    -
    -

    - Wooden Chair -

    -

    25,000₩ - Used

    -
    -
    -
    -
    -

    - Fur Slippers -

    -

    7,000₩ - New

    -
    -
    -
    -
    -

    - Red Backpack -

    -

    10,000₩ - Used

    -
    -
    -
    -
    -

    - Glass Bowl -

    -

    10,000₩ - Used

    -
    -
    -
    -
    -
    +
    ); } diff --git a/src/pages/product/components/ProductListComponent.js b/src/pages/product/components/ProductListComponent.js new file mode 100644 index 0000000..969544c --- /dev/null +++ b/src/pages/product/components/ProductListComponent.js @@ -0,0 +1,49 @@ +import styled from "@emotion/styled"; +import { useState } from "react"; +import ProductList from "./ProductList"; + +// 처음 진입하면 보이는 메인 페이지 +// api를 호출하여 ProcudtList에 정보를 넘겨서 보여주도록 함 +// 스크롤을 통해 더 많은 상품을 동적으로 로드 + +export default function ProductsListComponent({ + title, + param, + apiCallback, + queryKey +}) { + const [productList, setProductList] = useState([]); + const keyword = null; + const getProductList = async () => { + // pageParam : useInfiniteQuery의 getNextPageParam에서 반환해준 값 (=다음 불러올 페이지) + const resData = await apiCallback(param); + + const { page, lastPage, data: responseData } = resData.data; + setProductList((prevList) => [...prevList, ...responseData]); + + // return은 아래 useInfiniteQuery에서 getNextPageParam으로 전달 + // page 뜻을 전달하기 위해 이름 curPage로 전달 + return { curPage: page, lastPage }; + }; + + return ( +
    + {title} + + +
    + ); +} + +const ProductPageText = styled.h1` + text-align: center; + margin-top: 20px; + font-size: 18px; + font-weight: 700; + color: rgba(0, 0, 0, 0.8); +`; From 1eaac0fd4b6fdf3ba9480201b42cbaf245818822 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:16:43 +0900 Subject: [PATCH 32/46] refactor : useModalStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jsx를 받는 형식에서 type과 props로 수정하였음 --- src/components/popup/Modal.js | 70 ++++++-------------------- src/utils/hooks/store/useModalStore.js | 21 +++++--- 2 files changed, 30 insertions(+), 61 deletions(-) diff --git a/src/components/popup/Modal.js b/src/components/popup/Modal.js index 5d57e65..b1fc74d 100644 --- a/src/components/popup/Modal.js +++ b/src/components/popup/Modal.js @@ -1,67 +1,29 @@ import useModalStore from "@/utils/hooks/store/useModalStore"; -import styled from "@emotion/styled"; +import { lazy } from "react"; import Popup from "./Popup"; -/** - * Modal 컴포넌트는 팝업 UI를 제공합니다. - * 이 컴포넌트는 `useModalStore` 훅을 사용하여 팝업의 상태를 관리하며, - * `Popup` 컴포넌트를 사용하여 실제 모달을 렌더링합니다. - * - * - `useModalStore`를 통해 모달의 상태(isOpen, content 등)를 관리합니다. - * - 모달을 열고 닫는 동작은 `useModalStore`의 액션(openModal, closeModal)을 통해 수행됩니다. - * - 백드롭 클릭으로 모달을 닫는 동작은 `closeOnBackdrop` 상태에 따라 결정됩니다. - * - * 이 컴포넌트는 훅을 통한 상태 관리와 컴포넌트 렌더링을 분리하는 방식을 사용합니다. - * - * @returns {JSX.Element} 모달 컴포넌트. - */ +const DefaultModal = lazy(() => import("./modal/BaseModal")); +const AnotherModal = lazy(() => import("./modal/AnotherModal")); + +const modalComponents = { + default: DefaultModal, + anotherModalType: AnotherModal + // 다른 모달 타입에 대한 정의를 추가할 수 있습니다. +}; + const Modal = () => { - const { isOpen, content, closeOnBackdrop, closeModal } = useModalStore(); + const { isOpen, modalType, modalProps, closeOnBackdrop, closeModal } = + useModalStore(); const closePopup = closeOnBackdrop ? closeModal : null; + + const SelectedModal = modalComponents[modalType] || modalComponents.default; + return ( - {content} + ); }; export default Modal; - -//모달 컨텐츠 스타일 정의 -const ModalContent = styled.div` - background-color: white; - padding: 20px; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -`; - -/** - * 사용 예시: - * - * 아래 예시는 `Modal` 컴포넌트를 사용하여 사용자 정의 팝업을 생성하고 열기/닫기 기능을 구현합니다. - * - * ```javascript - * // `useModalStore`에서 필요한 함수를 가져옵니다. - * const { openModal, closeModal } = useModalStore(); - * - * // 팝업을 열기 위한 함수입니다. - * const openCustomPopup = () => { - * const customContent = ( - * <> - *

    Popup Title

    - *

    Popup Content

    - * - * - * ); - * openModal(customContent); // 백드롭 클릭으로 팝업을 닫습니다. - * openModal(customContent, false); // 백드롭 클릭으로 팝업을 닫지 않습니다. - * }; - * - * // 버튼 클릭 시 `openCustomPopup` 함수를 호출하여 팝업을 엽니다. - * - * ``` - * - * 위 예시에서 `openModal` 함수는 사용자 정의 컨텐츠를 받아 모달을 열고, - * `closeModal` 함수는 모달을 닫습니다. - */ diff --git a/src/utils/hooks/store/useModalStore.js b/src/utils/hooks/store/useModalStore.js index 6a25b95..96b2297 100644 --- a/src/utils/hooks/store/useModalStore.js +++ b/src/utils/hooks/store/useModalStore.js @@ -1,22 +1,29 @@ -// popupStore.js import { create } from "zustand"; /** * Popup 관련 상태를 관리하는 zustand 스토어. * * @property {boolean} isOpen - 팝업이 열려 있는지 여부. - * @property {React.ReactNode} content - 팝업 내에 렌더링할 컴포넌트. + * @property {string} modalType - 현재 활성화된 모달의 타입. + * @property {Object} modalProps - 현재 활성화된 모달에 전달될 props. * @property {boolean} closeOnBackdrop - 백드롭 클릭으로 팝업을 닫을지 여부. - * @property {Function} openModal - 팝업을 여는 함수. 컴포넌트와 백드롭 클릭 여부를 인자로 받음. + * @property {Function} openModal - 팝업을 여는 함수. 모달 타입과 모달 props, 백드롭 클릭 여부를 인자로 받음. * @property {Function} closeModal - 팝업을 닫는 함수. */ const useModalStore = create((set) => ({ isOpen: false, - content: null, + modalType: null, // 모달의 타입을 저장합니다. + modalProps: {}, // 모달에 전달될 props를 저장합니다. closeOnBackdrop: true, // 기본값은 true로 설정 - openModal: (content, closeOnBackdrop = true) => - set({ isOpen: true, content, closeOnBackdrop }), - closeModal: () => set({ isOpen: false, content: null, closeOnBackdrop: true }) + openModal: ({ modalType, modalProps = {}, closeOnBackdrop = true }) => + set({ isOpen: true, modalType, modalProps, closeOnBackdrop }), // 모달 타입과 props를 설정합니다. + closeModal: () => + set({ + isOpen: false, + modalType: null, + modalProps: {}, + closeOnBackdrop: true + }) // 모달 상태를 초기화합니다. })); export default useModalStore; From 01f7ae044a0854318d2a97f3b2b48353dfd6a304 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:17:00 +0900 Subject: [PATCH 33/46] =?UTF-8?q?feat=20:=20modal=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/modal/AnotherModal.js | 29 ++++++++++++++++++++++ src/components/popup/modal/BaseModal.js | 23 +++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/components/popup/modal/AnotherModal.js create mode 100644 src/components/popup/modal/BaseModal.js diff --git a/src/components/popup/modal/AnotherModal.js b/src/components/popup/modal/AnotherModal.js new file mode 100644 index 0000000..38dee3b --- /dev/null +++ b/src/components/popup/modal/AnotherModal.js @@ -0,0 +1,29 @@ +const AnotherModal = ({ + title, + content, + onConfirm, + onCancel, + confirmText = "확인", + cancelText = "취소" +}) => { + return ( +
    +

    {title}

    +

    {content}

    +
    + {onConfirm && ( + + )} + {onCancel && ( + + )} +
    +
    + ); +}; + +export default AnotherModal; diff --git a/src/components/popup/modal/BaseModal.js b/src/components/popup/modal/BaseModal.js new file mode 100644 index 0000000..92cc682 --- /dev/null +++ b/src/components/popup/modal/BaseModal.js @@ -0,0 +1,23 @@ +const BaseModal = (props) => { + // props 객체에서 필요한 값들을 추출합니다. + const { + title, + message, + onConfirm, + confirmText = "확인" // 기본값 설정 + } = props; + + return ( +
    +

    {title}

    +

    {message}

    +
    + +
    +
    + ); +}; + +export default BaseModal; From 539b8c02a30ade934a0e4dcd334611f173a3c659 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:17:32 +0900 Subject: [PATCH 34/46] =?UTF-8?q?fix=20:=20refactor=20modal=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/join/components/JoinForm.js | 213 ++++++++++++------------ src/pages/login/components/LoginForm.js | 25 +-- src/pages/product/ProductDetailPage.js | 29 +--- 3 files changed, 120 insertions(+), 147 deletions(-) diff --git a/src/pages/join/components/JoinForm.js b/src/pages/join/components/JoinForm.js index df6f59f..7bdded5 100644 --- a/src/pages/join/components/JoinForm.js +++ b/src/pages/join/components/JoinForm.js @@ -31,22 +31,15 @@ export const JoinForm = () => { } }; - const customContent = ( - <> -
    -

    - 가입에 {process ? "성공" : "실패"} 했습니다. -

    -

    {message}

    -
    - -
    -
    - - ); - openModal(customContent); // 백드롭 클릭으로 팝업을 닫습니다. + openModal({ + modalType: "default", + modalProps: { + title: `가입에 ${process ? "성공" : "실패"} 했습니다.`, + message, + confirmText: "확인", + onConfirm: handleConfirm + } + }); }; const handleSubmit = async (event) => { @@ -68,103 +61,101 @@ export const JoinForm = () => { }; return ( - <> -
    -
    -
    -
    -

    - 회원가입 -

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -

    - 이미 회원 가입하셨나요? -

    -
    navigate(ROUTES.HOME + "/" + ROUTES.LOGIN)} - className="text-blue-500 font-black hover:underline"> - Login here -
    +
    +
    +
    +
    +

    + 회원가입 +

    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +

    + 이미 회원 가입하셨나요? +

    +
    navigate(ROUTES.HOME + "/" + ROUTES.LOGIN)} + className="text-blue-500 font-black hover:underline"> + Login here
    - -
    +
    +
    -
    - +
    +
    ); }; diff --git a/src/pages/login/components/LoginForm.js b/src/pages/login/components/LoginForm.js index 21cba91..b67fcfb 100644 --- a/src/pages/login/components/LoginForm.js +++ b/src/pages/login/components/LoginForm.js @@ -32,22 +32,15 @@ export const LoginForm = () => { } }; - const customContent = ( - <> -
    -

    - 로그인에 {process ? "성공" : "실패"} 했습니다. -

    -

    {message}

    -
    - -
    -
    - - ); - openModal(customContent); // 백드롭 클릭으로 팝업을 닫습니다. + openModal({ + modalType: "default", + modalProps: { + title: `로그인에 ${process ? "성공" : "실패"} 했습니다.`, + message, + confirmText: "확인", + onConfirm: handleConfirm + } + }); // 백드롭 클릭으로 팝업을 닫습니다. }; const handleSubmit = async (event) => { diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index 392bd84..25ea193 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -16,26 +16,15 @@ export default function ProductDetailPage() { const openDialog = () => setIsDialogOpen(true); const closeDialog = () => setIsDialogOpen(false); const openCustomPopup = ({ process }) => { - const customContent = ( -
    -
    -

    - 가입에 {process ? "성공" : "실패"} 했습니다. -

    - {process ? ( -

    - 확인 버튼 클릭으로 로그인 페이지로 이동합니다. -

    - ) : ( -

    다시 시도해 주시기 바랍니다.

    - )} -
    - -
    -
    -
    - ); - openModal(customContent); // 백드롭 클릭으로 팝업을 닫습니다. + openModal({ + modalType: "anotherModalType", + modalProps: { + title: `장바구니에 저장했습니다.`, + message: "", + confirmText: "확인", + onConfirm: closeModal + } + }); // 백드롭 클릭으로 팝업을 닫습니다. }; return (
    From af981ab19df4231aad370c3038c6fa2ce29e5412 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:23:59 +0900 Subject: [PATCH 35/46] =?UTF-8?q?docs=20:=20modal=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=B2=95=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/Modal.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/popup/Modal.js b/src/components/popup/Modal.js index b1fc74d..ebe2a31 100644 --- a/src/components/popup/Modal.js +++ b/src/components/popup/Modal.js @@ -11,6 +11,34 @@ const modalComponents = { // 다른 모달 타입에 대한 정의를 추가할 수 있습니다. }; +/** + * Modal 컴포넌트는 팝업 UI를 제공합니다. + * 이 컴포넌트는 `useModalStore` 훅을 사용하여 팝업의 상태를 관리하며, + * `Popup` 컴포넌트를 사용하여 실제 모달을 렌더링합니다. + * + * - `useModalStore`를 통해 모달의 상태(isOpen, content 등)를 관리합니다. + * - 모달을 열고 닫는 동작은 `useModalStore`의 액션(openModal, closeModal)을 통해 수행됩니다. + * - 백드롭 클릭으로 모달을 닫는 동작은 `closeOnBackdrop` 상태에 따라 결정됩니다. + * + * 이 컴포넌트는 훅을 통한 상태 관리와 컴포넌트 렌더링을 분리하는 방식을 사용합니다. + * + * @returns {JSX.Element} 모달 컴포넌트. + * + * @example + * // 사용법 예시: + * const { openModal, closeModal } = useModalStore(); + * + * // 커스텀 팝업 열기 + * openModal({ + * modalType: "default", + * modalProps: { + * title: `가입에 성공했습니다.`, + * message, + * confirmText: "확인", + * onConfirm: closeModal + * } + * }); + */ const Modal = () => { const { isOpen, modalType, modalProps, closeOnBackdrop, closeModal } = useModalStore(); From 3a08a1383dd5b131de27a860f3285b1a09e66d5f Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:34:18 +0900 Subject: [PATCH 36/46] =?UTF-8?q?refactor=20:=20modal=20css=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20emotion=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/Popup.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/popup/Popup.js b/src/components/popup/Popup.js index 11998b4..e504454 100644 --- a/src/components/popup/Popup.js +++ b/src/components/popup/Popup.js @@ -1,6 +1,6 @@ // Popup.js +import { Global, css } from "@emotion/react"; import styled from "@emotion/styled"; -import { useEffect } from "react"; import { createPortal } from "react-dom"; /** @@ -17,24 +17,22 @@ import { createPortal } from "react-dom"; * @returns {JSX.Element|null} 팝업 컴포넌트. */ const Popup = ({ isOpen, closePopup, children }) => { - useEffect(() => { - const body = document.body; - // 팝업이 열려 있을 때 body 스크롤을 막습니다. - if (isOpen) { - body.style.overflow = "hidden"; // 스크롤 방지 - } - return () => { - // 컴포넌트가 언마운트될 때 스크롤을 다시 허용합니다. - body.style.overflow = "visible"; // 스크롤 허용 - }; - }, [isOpen]); - if (!isOpen) return null; - return createPortal( - // 팝업 배경을 클릭하면 팝업을 닫습니다. - {children}, - document.getElementById("popup-root") // 팝업을 렌더링할 DOM 노드를 지정합니다. + return ( + <> + + {createPortal( + {children}, + document.getElementById("popup-root") // 팝업을 렌더링할 DOM 노드를 지정합니다. + )} + ); }; From abaf746bd69d7adb72b52f5b4b940833d5327b25 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Wed, 7 Feb 2024 00:04:07 +0900 Subject: [PATCH 37/46] =?UTF-8?q?style=20:=20css=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/Dialog.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/popup/Dialog.js b/src/components/popup/Dialog.js index 731849a..77cb6d1 100644 --- a/src/components/popup/Dialog.js +++ b/src/components/popup/Dialog.js @@ -1,5 +1,5 @@ /** @jsxImportSource @emotion/react */ -import { css } from "@emotion/react"; +import { css, Global } from "@emotion/react"; import { useEffect, useRef } from "react"; /** @@ -20,18 +20,10 @@ export default function Dialog({ isOpen, onBackdropClick = null, children }) { const dialogRef = useRef(null); useEffect(() => { - const body = document.body; - if (isOpen) { - // 대화 상자를 모달로 표시합니다. - dialogRef.current.showModal(); - // 스크롤을 방지하기 위해 body의 overflow를 hidden으로 설정합니다. - body.style.overflow = "hidden"; + dialogRef.current?.showModal(); // 모달 열기 } else { - // 대화 상자를 닫습니다. - dialogRef.current.close(); - // 스크롤을 허용하기 위해 body의 overflow를 unset으로 설정합니다. - body.style.overflow = "unset"; + dialogRef.current?.close(); // 모달 닫기 } // 배경을 클릭했을 때 대화 상자를 닫는 이벤트 핸들러입니다. @@ -56,9 +48,12 @@ export default function Dialog({ isOpen, onBackdropClick = null, children }) { }, [isOpen]); return ( - - {children} - + <> + + + {children} + + ); } @@ -77,6 +72,15 @@ const dialogStyle = (isOpen) => css` /* 여기에 필요한 다른 dialog 스타일을 추가할 수 있습니다 */ `; +const globalStyles = (isOpen) => css` + ${isOpen && + ` + body { + overflow: hidden; + } + `} +`; + /** * 사용법: * 1. isOpen 상태를 정의하여 모달이 열려 있는지 여부를 제어합니다. From 5d2577f02395cbc5268df85ab0778722259d927d Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 08:23:14 +0900 Subject: [PATCH 38/46] =?UTF-8?q?refactor:=20auth=20=ED=86=B5=EC=8B=A0=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=EC=8B=9C=EC=97=90=EB=A7=8C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/hooks/store/useAuthStore.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/utils/hooks/store/useAuthStore.js b/src/utils/hooks/store/useAuthStore.js index 54f80dd..3440076 100644 --- a/src/utils/hooks/store/useAuthStore.js +++ b/src/utils/hooks/store/useAuthStore.js @@ -19,18 +19,19 @@ const useAuthStore = create( logout: async () => { try { // 서버의 로그아웃 엔드포인트에 요청 - await marketApi.post("/auth/logout"); + const res = await marketApi.post("/auth/logout"); + if (res.status === 200) { + set({ + accessToken: null, + isAuthenticated: false, + userName: null, + id: null, + email: null + }); + } } catch (error) { console.error("Logout failed:", error); } - // 클라이언트 상태 업데이트 - set({ - accessToken: null, - isAuthenticated: false, - userName: null, - id: null, - email: null - }); } })) ); From ac38afa6aae7b6be122d2fb850d7c129a7f9a4ea Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 08:23:52 +0900 Subject: [PATCH 39/46] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/hooks/store/useAuthStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/hooks/store/useAuthStore.js b/src/utils/hooks/store/useAuthStore.js index 3440076..3d78251 100644 --- a/src/utils/hooks/store/useAuthStore.js +++ b/src/utils/hooks/store/useAuthStore.js @@ -3,8 +3,8 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; // 로그아웃 사용법 -// const auth = useAuthStore(); -//
    Logout
    +// const { logout } = useAuthStore(); +//
    logout()}>Logout
    const useAuthStore = create( devtools((set) => ({ From 0a934932ebef672bcf7aa263b0f6bbff862cf7fa Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:17:13 +0900 Subject: [PATCH 40/46] =?UTF-8?q?refactor=20:=20preFetching=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/loader/productLoader.js | 13 +++++++++++++ src/pages/product/ProductDetailPage.js | 18 +++++++----------- src/router.js | 12 ++++++++---- 3 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 src/api/loader/productLoader.js diff --git a/src/api/loader/productLoader.js b/src/api/loader/productLoader.js new file mode 100644 index 0000000..8840f55 --- /dev/null +++ b/src/api/loader/productLoader.js @@ -0,0 +1,13 @@ +// productLoader.js +import { getPostById } from "../marketApi"; + +export const productLoader = async ({ params }) => { + try { + console.log(params.productid, "params.productid", params); + const response = await getPostById(params.productid); + return response.data; + } catch (e) { + console.log(e); + return { error: "Error", message: e.message }; + } +}; diff --git a/src/pages/product/ProductDetailPage.js b/src/pages/product/ProductDetailPage.js index 49516d4..8317ea0 100644 --- a/src/pages/product/ProductDetailPage.js +++ b/src/pages/product/ProductDetailPage.js @@ -1,22 +1,18 @@ -import { getPostById, getPublishedPosts } from "@/api/marketApi"; +import { getPublishedPosts } from "@/api/marketApi"; import ProductImageCarousel from "@/components/carousel/ProductImageCarousel"; import useModalStore from "@/utils/hooks/store/useModalStore"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import { useLoaderData, useParams } from "react-router-dom"; import ProductsListComponent from "./components/ProductListComponent"; export default function ProductDetailPage() { const { openModal } = useModalStore(); + const loaderData = useLoaderData(); + console.log(loaderData, "loaderData"); const { productid } = useParams(); - const [product, setProduct] = useState({}); + const product = loaderData; console.log(productid, "id"); - useEffect(() => { - getPostById(productid).then((e) => { - console.log(e.data); - setProduct(e.data); - }); - }, []); + const { title, price, content, imgUrls = [], location } = product; console.log(imgUrls, "imgUrls"); @@ -35,7 +31,7 @@ export default function ProductDetailPage() { openModal(customContent); // 백드롭 클릭으로 팝업을 닫습니다. }; return ( -
    +
    diff --git a/src/router.js b/src/router.js index 1c15d2a..5ba7fb1 100644 --- a/src/router.js +++ b/src/router.js @@ -3,14 +3,14 @@ import Header from "@/components/header"; import NotFoundPage from "@/pages/NotFoundPage"; import ProductsPage from "@/pages/product/ProductsPage"; import { ROUTES } from "@/utils/constants/routePaths"; -import { createBrowserRouter } from "react-router-dom"; +import { Navigate, createBrowserRouter } from "react-router-dom"; +import { productLoader } from "./api/loader/productLoader"; import { JoinPage } from "./pages/join/JoinPage"; import { LoginPage } from "./pages/login/LoginPage"; import ProductDetailPage from "./pages/product/ProductDetailPage"; -import ProductsSearchPage from "./pages/product/ProductsSearchPage"; import ProductSearchResultPage from "./pages/product/ProductSearchResultPage"; +import ProductsSearchPage from "./pages/product/ProductsSearchPage"; import { ProfilePage } from "./pages/profile/ProfilePage"; -import { Navigate } from "react-router-dom"; import useAuthStore from "./utils/hooks/store/useAuthStore"; // 코드 스플리팅을 위해 React.lazy를 사용하는 주석 처리된 예시입니다. @@ -24,7 +24,11 @@ export const routeConfig = [ // 홈 페이지 경로, ProductPage 컴포넌트를 렌더링합니다. // `index: true`는 이 라우트가 자식 라우트보다 우선순위가 높음을 나타냅니다. { path: ROUTES.HOME, element: , index: true }, - { path: ROUTES.PRODUCT, element: }, + { + path: ROUTES.PRODUCT, + element: , + loader: productLoader + }, { path: ROUTES.SEARCH, element: }, { path: ROUTES.SEARCH_RESULT, element: }, { path: ROUTES.LOGIN, element: }, From 5db2ffe1ab7fb7c81ead1519edeacc3139caeea0 Mon Sep 17 00:00:00 2001 From: nakjun <111031253+nakjun12@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:43:53 +0900 Subject: [PATCH 41/46] =?UTF-8?q?feat=20:=20modal.md=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/modal.md | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/components/popup/modal.md diff --git a/src/components/popup/modal.md b/src/components/popup/modal.md new file mode 100644 index 0000000..8f23a2e --- /dev/null +++ b/src/components/popup/modal.md @@ -0,0 +1,61 @@ +# 모달 컴포넌트 + +## 개요 + +`Modal` 컴포넌트는 사용자 인터페이스에 팝업 형태의 UI를 제공합니다. 모달의 상태 관리를 위해 `useModalStore` 훅을 활용하며, `Popup` 컴포넌트를 사용하여 실제 모달 내용을 렌더링합니다. + +### 핵심 기능 + +- **상태 관리**: `useModalStore` 훅을 통해 모달의 상태(예: 열림/닫힘 상태, 내용 등)를 관리합니다. +- **액션 처리**: 모달의 열기와 닫기 동작은 `useModalStore` 내의 `openModal` 및 `closeModal` 액션을 통해 처리됩니다. +- **백드롭 클릭**: `closeOnBackdrop` 속성에 따라 백드롭(모달 외부) 클릭 시 모달을 닫을지 결정합니다. + +### 컴포넌트 구성 + +- `DefaultModal`: 기본 모달 컴포넌트로, 기본적인 모달 UI를 제공합니다. +- `AnotherModal`: 다양한 시나리오나 스타일에 맞춰 커스터마이징 가능한 추가 모달 컴포넌트입니다. + +## 사용 방법 + +```jsx +import { useModalStore } from "@/utils/hooks/store/useModalStore"; +import Modal from "@/components/popup/Modal"; + +const App = () => { + const { openModal, closeModal } = useModalStore(); + + const handleOpenModal = () => { + openModal({ + modalType: "default", + modalProps: { + title: "가입에 성공했습니다.", + message: "환영합니다!", + confirmText: "확인", + onConfirm: closeModal + } + }); + }; + + return ( +
    + + +
    + ); +}; +``` + +위 예시는 `openModal` 함수를 사용하여 모달을 열고, 모달 내의 확인 버튼을 통해 `closeModal` 함수를 호출하여 모달을 닫는 과정을 보여줍니다. + +## 컴포넌트 속성 + +- `isOpen`: 모달의 열림 및 닫힘 상태를 제어하는 불리언 값입니다. +- `modalType`: 렌더링할 모달 컴포넌트의 유형을 지정합니다. (`default`, `anotherModalType` 등) +- `modalProps`: 모달 컴포넌트로 전달될 속성들을 지정하는 객체입니다. +- `closeOnBackdrop`: 백드롭 클릭 시 모달을 닫을지 여부를 제어하는 불리언 값입니다. + +## 추가 커스터마이제이션 + +모달 컴포넌트를 추가로 커스터마이즈하고자 한다면, `modalComponents` 객체에 새로운 모달 유형과 해당 컴포넌트를 정의하세요. 각 모달 컴포넌트는 필요에 따라 특정 스타일이나 기능을 포함할 수 있습니다. + +이 문서는 `Modal` 컴포넌트의 주요 기능, 사용법, 속성 및 확장 방법에 대해 설명합니다. 필요에 따라 더 많은 세부 정보나 추가 섹션을 포함하여 문서를 확장할 수 있습니다. From 699d0ff91032106db9c3c8d95351f9a4b519d8e7 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 10:12:36 +0900 Subject: [PATCH 42/46] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20ap?= =?UTF-8?q?i=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/marketApi.js | 65 +++++++++++++++++++---------------- src/utils/LocationObserver.js | 46 ++++++++++++++++--------- 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/api/marketApi.js b/src/api/marketApi.js index 4465cc1..0701b29 100644 --- a/src/api/marketApi.js +++ b/src/api/marketApi.js @@ -27,41 +27,48 @@ marketApi.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; - // 리프레시 토큰 요청에서의 401 오류인지 확인 - const isRefreshTokenRequest = originalRequest.url === "/auth/refresh-token"; - - // 401 인증에러 + 재요청이 아닐때 + refreshToken에 대한 요청이 아닐 떄 - // 즉 액세스 토큰이 만료되어 거절될 떄 재발급 절차 - if ( - error.response.status === 401 && - !originalRequest._retry && - !isRefreshTokenRequest - ) { + const isRefreshTokenRequest = originalRequest.url.includes( + "/auth/refresh-token" + ); + + // 리프레시 토큰 요청에서 401 오류가 발생한 경우 토큰 만료등 일반적인 이유 + if (isRefreshTokenRequest && error.response?.status === 401) { + useAuthStore.getState().logout(); + return Promise.reject(error); // 여기서 얼리 리턴 + } + + // 다른 요청에서 401 오류 처리 (엑세스 토큰의 만료) + if (error.response?.status === 401 && !originalRequest._retry) { + // 무한 재요청 방지를 위한 트리거 originalRequest._retry = true; - try { - const refreshData = await marketApi.post("/auth/refresh-token"); - useAuthStore.getState().setAccessToken(refreshData.data?.accessToken); - originalRequest.headers["Authorization"] = - `Bearer ${refreshData.data?.accessToken}`; - const userData = await marketApi.get("/users/me", { - requiresAuth: true - }); - useAuthStore.getState().setUser({ - id: userData.data?.id, - userName: userData.data?.username - }); - return marketApi(originalRequest); - } catch (refreshError) { - // 리프레시 토큰 요청 실패 시 로그아웃 처리 - console.error("인증 정보 만료"); - useAuthStore.getState().logout(); - return Promise.reject(refreshError); + const newAccessToken = await refreshAccessTokenAndFetchUser(); + if (newAccessToken) { + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + return marketApi(originalRequest); // 재요청 } } - return Promise.reject(error); + + return Promise.reject(error); // 다른 모든 에러는 여기서 처리 } ); +// 리프래쉬 토큰에 대한 예외 처리 함수 +async function refreshAccessTokenAndFetchUser() { + try { + const { data } = await marketApi.post("/auth/refresh-token"); + if (data.accessToken) { + useAuthStore.getState().setAccessToken(data.accessToken); + return data.accessToken; + } + } catch (error) { + if (error.response?.status !== 401) { + console.error("인증 정보 갱신 실패"); + } + useAuthStore.getState().logout(); + return null; + } +} + /** * 사용자 (user)에 관련된 정보를 이용한 api입니다. * diff --git a/src/utils/LocationObserver.js b/src/utils/LocationObserver.js index 9489a50..2acffec 100644 --- a/src/utils/LocationObserver.js +++ b/src/utils/LocationObserver.js @@ -2,36 +2,48 @@ import { useEffect } from "react"; import useAuthStore from "./hooks/store/useAuthStore"; import marketApi from "@/api/marketApi"; -// 최상단에서 감지하는 함수 변경 +// 최상단에서 첫 로드, accessToken을 감시하는 함수 export function LocationObserver() { const { setAccessToken, setUser, accessToken } = useAuthStore(); useEffect(() => { - // 첫 기동시 한번 refreshToken 체크, 이후 accessToken이 변경되면 체크 + // 첫 화면 로드시 refreshToken 체크 const fetchRefresh = async () => { - const { data } = await marketApi.post("/auth/refresh-token"); - if (data?.accessToken) { - setAccessToken(data.accessToken); + try { + const { data } = await marketApi.post("/auth/refresh-token"); + if (data?.accessToken) { + setAccessToken(data.accessToken); + } + } catch (error) { + // 401 Unauthorized 에러일 경우 콘솔에 에러를 출력하지 않음 + if (error.response?.status !== 401) { + console.error(error); + } + // 필요한 경우 여기에 리프레시 토큰 실패에 대한 추가적인 처리 로직을 구현할 수 있습니다. } }; fetchRefresh(); }, []); useEffect(() => { - // 첫 기동시 한번 refreshToken 체크, 이후 accessToken이 변경되면 체크 + // accessToken이 새로 추가되거나 변경되면 개인 정보 갱신 const fetchUser = async () => { - const userData = await marketApi.get("/users/me", { - requiresAuth: true - }); - setUser({ - id: userData.data?.id, - userName: userData.data?.username, - email: userData.data?.email - }); + if (accessToken) { + try { + const userData = await marketApi.get("/users/me", { + requiresAuth: true + }); + setUser({ + id: userData.data?.id, + userName: userData.data?.username, + email: userData.data?.email + }); + } catch (error) { + console.error(error); + } + } }; - if (accessToken) { - fetchUser(); - } + fetchUser(); }, [accessToken]); // 실제 UI를 렌더링할 필요가 없으므로, null을 반환합니다. From f043845af550d384e361e3a556cbcf5570e851d2 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 10:20:15 +0900 Subject: [PATCH 43/46] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EC=8B=9C=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=A0=88=EC=B0=A8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/join/components/JoinForm.js | 21 ++++++++------------- src/pages/login/components/LoginForm.js | 21 ++++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/pages/join/components/JoinForm.js b/src/pages/join/components/JoinForm.js index 7bdded5..45b0d94 100644 --- a/src/pages/join/components/JoinForm.js +++ b/src/pages/join/components/JoinForm.js @@ -11,33 +11,28 @@ export const JoinForm = () => { const { mutate, isPending } = useMutation({ mutationFn: postAuthSignup, onSuccess: () => { - openCustomPopup({ + navigate(ROUTES.HOME); + return openCustomPopup({ process: true, - message: "가입에 성공했습니다. 확인으로 메인페이지로 이동합니다." + message: "가입에 성공했습니다." }); }, onError: (error) => { - openCustomPopup({ process: false, message: error.response.data.message }); + return openCustomPopup({ + process: false, + message: error.response.data.message + }); } }); const openCustomPopup = ({ process, message }) => { - const handleConfirm = () => { - // 성공 실패시 - if (process) { - navigate(ROUTES.HOME); - } else { - closeModal(); - } - }; - openModal({ modalType: "default", modalProps: { title: `가입에 ${process ? "성공" : "실패"} 했습니다.`, message, confirmText: "확인", - onConfirm: handleConfirm + onConfirm: closeModal() } }); }; diff --git a/src/pages/login/components/LoginForm.js b/src/pages/login/components/LoginForm.js index b67fcfb..bfb447e 100644 --- a/src/pages/login/components/LoginForm.js +++ b/src/pages/login/components/LoginForm.js @@ -12,33 +12,28 @@ export const LoginForm = () => { const { mutate, isPending } = useMutation({ mutationFn: postAuthLogin, onSuccess: () => { - openCustomPopup({ + navigate(ROUTES.HOME); + return openCustomPopup({ process: true, - message: "로그인에 성공했습니다. 확인으로 메인페이지로 이동합니다." + message: "로그인에 성공했습니다." }); }, onError: (error) => { - openCustomPopup({ process: false, message: error.response.data.message }); + return openCustomPopup({ + process: false, + message: error.response.data.message + }); } }); const openCustomPopup = ({ process, message }) => { - const handleConfirm = () => { - // 성공 실패시 - if (process) { - navigate(ROUTES.HOME); - } else { - closeModal(); - } - }; - openModal({ modalType: "default", modalProps: { title: `로그인에 ${process ? "성공" : "실패"} 했습니다.`, message, confirmText: "확인", - onConfirm: handleConfirm + onConfirm: closeModal() } }); // 백드롭 클릭으로 팝업을 닫습니다. }; From db770bc4cc512c0e98876a72fc5bb5aad909b9d4 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 10:26:22 +0900 Subject: [PATCH 44/46] =?UTF-8?q?feat:=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/marketApi.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api/marketApi.js b/src/api/marketApi.js index 0701b29..9106cb6 100644 --- a/src/api/marketApi.js +++ b/src/api/marketApi.js @@ -30,15 +30,16 @@ marketApi.interceptors.response.use( const isRefreshTokenRequest = originalRequest.url.includes( "/auth/refresh-token" ); + const isAuthenticatedError = error.response?.status === 401; - // 리프레시 토큰 요청에서 401 오류가 발생한 경우 토큰 만료등 일반적인 이유 - if (isRefreshTokenRequest && error.response?.status === 401) { + // 401에러 + Refresh token 에러 (만료 혹은 없거나 비적합) + if (isAuthenticatedError && isRefreshTokenRequest) { useAuthStore.getState().logout(); - return Promise.reject(error); // 여기서 얼리 리턴 + return Promise.reject(error); } - // 다른 요청에서 401 오류 처리 (엑세스 토큰의 만료) - if (error.response?.status === 401 && !originalRequest._retry) { + // 401에러 + 재시도이지 않은 에러 + if (isAuthenticatedError && !originalRequest._retry) { // 무한 재요청 방지를 위한 트리거 originalRequest._retry = true; const newAccessToken = await refreshAccessTokenAndFetchUser(); @@ -52,7 +53,7 @@ marketApi.interceptors.response.use( } ); -// 리프래쉬 토큰에 대한 예외 처리 함수 +// 리프래쉬 토큰에 대한 함수 async function refreshAccessTokenAndFetchUser() { try { const { data } = await marketApi.post("/auth/refresh-token"); @@ -62,7 +63,8 @@ async function refreshAccessTokenAndFetchUser() { } } catch (error) { if (error.response?.status !== 401) { - console.error("인증 정보 갱신 실패"); + // 401 만료 이외에 통신 자체 에러일 경우 에러표출 + console.error(error); } useAuthStore.getState().logout(); return null; From b159fe1f907af917394e9dae4879a22fd9a12ac8 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 10:46:16 +0900 Subject: [PATCH 45/46] =?UTF-8?q?refactor:=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0,=20=EB=B0=9C=ED=91=9C=EC=9A=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/marketApi.js | 40 ++++++++++--------- .../header/components/LogoutMenuList.js | 4 ++ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/api/marketApi.js b/src/api/marketApi.js index 9106cb6..95a7b24 100644 --- a/src/api/marketApi.js +++ b/src/api/marketApi.js @@ -26,26 +26,28 @@ marketApi.interceptors.request.use( marketApi.interceptors.response.use( (response) => response, async (error) => { - const originalRequest = error.config; - const isRefreshTokenRequest = originalRequest.url.includes( - "/auth/refresh-token" - ); - const isAuthenticatedError = error.response?.status === 401; - - // 401에러 + Refresh token 에러 (만료 혹은 없거나 비적합) - if (isAuthenticatedError && isRefreshTokenRequest) { - useAuthStore.getState().logout(); - return Promise.reject(error); - } + // 401에러의 경우 (인증 에러) + if (error.response?.status === 401) { + const isRefreshTokenRequest = originalRequest.url.includes( + "/auth/refresh-token" + ); + const originalRequest = error.config; + + // Refresh token 에러 (만료 혹은 없거나 비적합) + if (isRefreshTokenRequest) { + useAuthStore.getState().logout(); + return Promise.reject(error); + } - // 401에러 + 재시도이지 않은 에러 - if (isAuthenticatedError && !originalRequest._retry) { - // 무한 재요청 방지를 위한 트리거 - originalRequest._retry = true; - const newAccessToken = await refreshAccessTokenAndFetchUser(); - if (newAccessToken) { - originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; - return marketApi(originalRequest); // 재요청 + // 재시도이지 않은 에러 + if (!originalRequest._retry) { + // 무한 재요청 방지를 위한 트리거 + originalRequest._retry = true; + const newAccessToken = await refreshAccessTokenAndFetchUser(); + if (newAccessToken) { + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + return marketApi(originalRequest); // 재요청 + } } } diff --git a/src/components/header/components/LogoutMenuList.js b/src/components/header/components/LogoutMenuList.js index 45b9acb..3f10621 100644 --- a/src/components/header/components/LogoutMenuList.js +++ b/src/components/header/components/LogoutMenuList.js @@ -1,3 +1,4 @@ +import { getUserMe } from "@/api/marketApi"; import { ROUTES } from "@/utils/constants/routePaths"; import useAuthStore from "@/utils/hooks/store/useAuthStore"; import { useNavigate } from "react-router-dom"; @@ -30,6 +31,9 @@ export const LogoutMenuList = () => {
  • logout()}>
    Logout
  • +
  • getUserMe()}> +
    Me
    +
  • ); From 39efff704ad995ee4f4531acf3fc637c81bbdd23 Mon Sep 17 00:00:00 2001 From: "Sunghum Paik (Brian)" Date: Wed, 7 Feb 2024 11:09:12 +0900 Subject: [PATCH 46/46] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20import=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/product/components/ProductList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/product/components/ProductList.js b/src/pages/product/components/ProductList.js index 60d205d..b6eb3cd 100644 --- a/src/pages/product/components/ProductList.js +++ b/src/pages/product/components/ProductList.js @@ -2,7 +2,6 @@ import Loading from "@/components/Loading"; import useModalStore from "@/utils/hooks/store/useModalStore"; import { css } from "@emotion/react"; import styled from "@emotion/styled"; -import { css } from "@emotion/react"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom";