From c7647c4eebe2e7614e2b4d8900ae184573e813f4 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Wed, 30 Mar 2022 18:00:17 +0900 Subject: [PATCH 1/8] =?UTF-8?q?conf:=20Image=20src=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20next.config.js=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 참고: https://nextjs.org/docs/messages/next-image-unconfigured-host unconfigured host로 된 static 서버에 image를 요청할 수 없는 문제 해결 --- next.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/next.config.js b/next.config.js index ef9a580..9519f8d 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,9 @@ const nextConfig = { return config; }, + images: { + domains: ['spoonacular.com'], + }, }; const sentryWebpackPluginOptions = { From 70d1605860a487586c70130c05ab3c9b24c81f26 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Wed, 30 Mar 2022 18:01:39 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20Pagination=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20currentPage=20=EB=B0=94=EA=BE=B8?= =?UTF-8?q?=EB=8A=94=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - currentPage 바꾸는 핸들러 추가 --- src/components/Pagination/Pagination.styled.tsx | 7 ++++--- src/components/Pagination/Pagination.tsx | 2 +- src/components/Pagination/Pagination.types.ts | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/Pagination/Pagination.styled.tsx b/src/components/Pagination/Pagination.styled.tsx index 5192b8b..851d276 100644 --- a/src/components/Pagination/Pagination.styled.tsx +++ b/src/components/Pagination/Pagination.styled.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { pxToRem } from 'utils'; +import { PageButtonProps } from './Pagination.types'; export const StyledPaginationControl = styled.div` display: flex; @@ -14,11 +15,11 @@ export const StyledPaginationControl = styled.div` } `; -export const StyledPageButton = styled.button` - color: ${({ theme, current }) => (current ? theme.color.white : theme.color.primaryOrange)}; +export const StyledPageButton = styled.button` + color: ${({ theme, $current }) => ($current ? theme.color.white : theme.color.primaryOrange)}; border: 1px solid ${({ theme }) => theme.color.primaryOrange}; border-radius: ${pxToRem(5)}; - background-color: ${({ theme, current }) => (current ? theme.color.primaryOrange : theme.color.white)}; + background-color: ${({ theme, $current }) => ($current ? theme.color.primaryOrange : theme.color.white)}; width: ${pxToRem(32)}; height: ${pxToRem(32)}; margin: ${pxToRem(6)}; diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index 0c31005..6185542 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -23,7 +23,7 @@ export const Pagination = ({ limit, currentPage, onClick: handleClick, totalResu
  • { diff --git a/src/components/Pagination/Pagination.types.ts b/src/components/Pagination/Pagination.types.ts index 83777a3..a992799 100644 --- a/src/components/Pagination/Pagination.types.ts +++ b/src/components/Pagination/Pagination.types.ts @@ -3,4 +3,8 @@ export interface PaginationProps { onClick: (currentPage: number) => void; totalResults: number; limit: number; +} + +export interface PageButtonProps { + $current?: boolean; } \ No newline at end of file From 0473beb636440ccc33abe56d5e64567fedd67fb7 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Wed, 30 Mar 2022 18:44:12 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20Header=20hide=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=8B=A4=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - or로 해야할 것을 and로 해놨던 실수가 있었습니다. - $hide 프롭을 isloading || hideHeader로 변경 --- src/components/Header/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index ca7db9c..0398c59 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -55,7 +55,7 @@ export const Header = (): JSX.Element => { }, []); return ( - + From aa56914cd431e39a3a28f79111a42b963a87dc92 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Wed, 30 Mar 2022 19:09:40 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20Search=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1페이지는 SSR로, 2번부터는 CSR로 가져와 렌더링하는 로직 추가 - 첫 페이지에서 fetching 못하게 하려다보니 코드가 다소 난잡해져서 리팩토링 필요 - currentPage에서 바뀌어도 리스트가 렌더링되지 않는 기현상 발생 TODO: - current page 바뀌면 리스트 업데이트 - CardList 컴포넌트 만들 필요 --- src/components/index.ts | 3 +- src/pages/search/[keyword].tsx | 71 ++++++++++++++++++++--------- src/pages/search/search.types.ts | 1 + src/store/services/index.ts | 7 ++- src/store/services/types/queries.ts | 25 ++++++++++ 5 files changed, 83 insertions(+), 24 deletions(-) diff --git a/src/components/index.ts b/src/components/index.ts index 14b8d2e..1c74d07 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,4 +17,5 @@ export * from './ErrorBoundary/ErrorBoundary'; export * from './Card/Card'; export * from './RandomRecipe/RandomRecipe'; export * from './HotRecipes/HotRecipes'; -export * from './Toast/Toast'; \ No newline at end of file +export * from './Toast/Toast'; +export * from './Pagination/Pagination'; diff --git a/src/pages/search/[keyword].tsx b/src/pages/search/[keyword].tsx index 0bb0074..a43849a 100644 --- a/src/pages/search/[keyword].tsx +++ b/src/pages/search/[keyword].tsx @@ -1,30 +1,59 @@ -import { useRouter } from 'next/router'; +import { Loading, Card, Pagination } from 'components'; import { NextPage } from 'next'; +import Head from 'next/head'; +import { useEffect, useState } from 'react'; import { useSearchRecipeQuery } from 'store/services'; -import { useState } from 'react'; import { ContextProp, SearchPageProps } from './search.types'; + const RESULTS_PER_PAGE = 12; -const Search: NextPage = ({ results, totalResults }: SearchPageProps) => { +const Search: NextPage = ({ keyword, results, totalResults }) => { + const [currentPage, setCurrentPage] = useState(1); + const [currentResults, setCurrentResults] = useState([]); + const { data, error, isLoading } = useSearchRecipeQuery({ + keyword, + number: RESULTS_PER_PAGE, + offset: (currentPage - 1) * RESULTS_PER_PAGE, + }); + + useEffect(() => { + if (currentPage !== 1 && data) setCurrentResults(data.results); + }, [currentPage]); + + const handleClick = (page: number) => { + setCurrentPage(page); + }; - // const { - // query: { keyword }, - // } = useRouter(); - // const [currentIndex, setCurrentIndex] = useState(0); - // const { data, error, isLoading } = useSearchRecipeQuery({ - // keyword, - // number: RESULTS_PER_PAGE, - // offset: (currentIndex - 1) * RESULTS_PER_PAGE, - // }); - // console.log(data); return (
    -

    {totalResults}

    -
      - {results.map(({ id, title }) => ( -
    • {title}
    • - ))} -
    + + {`Searched: ${keyword}`} + + {currentPage !== 1 && isLoading ? ( + + ) : ( +
      + {(currentPage === 1 ? results : currentResults).map(({ id, title, image }) => ( +
    • + +
    • + ))} +
    + )} +
    ); }; @@ -32,7 +61,7 @@ const Search: NextPage = ({ results, totalResults }: SearchPageProps) => { export async function getServerSideProps({ query }: ContextProp) { const { keyword } = query; const { results, totalResults } = await fetch( - `https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com//recipes/search?query=${keyword}&number=${RESULTS_PER_PAGE}&offset=${0}`, + `https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/search?query=${keyword}&number=${RESULTS_PER_PAGE}&offset=${0}`, { headers: { 'content-type': 'application/json', @@ -42,7 +71,7 @@ export async function getServerSideProps({ query }: ContextProp) { } as RequestInit, ).then((res) => res.json()); return { - props: { results, totalResults }, + props: { keyword, results, totalResults }, }; } diff --git a/src/pages/search/search.types.ts b/src/pages/search/search.types.ts index 2db65f7..1244bc6 100644 --- a/src/pages/search/search.types.ts +++ b/src/pages/search/search.types.ts @@ -13,6 +13,7 @@ interface SearchResult { } export interface SearchPageProps { + keyword: string; results: SearchResult[]; totalResults: number; } diff --git a/src/store/services/index.ts b/src/store/services/index.ts index dd05900..3ad5112 100644 --- a/src/store/services/index.ts +++ b/src/store/services/index.ts @@ -1,5 +1,5 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import { RandomRecipeQuery } from './types/queries'; +import { RandomRecipeQuery, SearchQuery, SearchResults } from './types/queries'; export const twoSpoonApi = createApi({ reducerPath: 'twoSpoonApi', @@ -17,7 +17,10 @@ export const twoSpoonApi = createApi({ getRandomRecipe: builder.query({ query: (number = 1) => `recipes/random?number=${number}`, }), + searchRecipe: builder.query({ + query: ({ keyword, number, offset }) => `recipes/search?query=${keyword}&number=${number}&offset=${offset}`, + }), }), }); -export const { useGetRandomRecipeQuery } = twoSpoonApi; +export const { useGetRandomRecipeQuery, useSearchRecipeQuery } = twoSpoonApi; diff --git a/src/store/services/types/queries.ts b/src/store/services/types/queries.ts index fca0f69..dff744a 100644 --- a/src/store/services/types/queries.ts +++ b/src/store/services/types/queries.ts @@ -8,3 +8,28 @@ export interface RandomRecipe { export interface RandomRecipeQuery { recipes: RandomRecipe[]; } + +interface SearchRecipeItem { + id: number; + title: string; + readyInMinutes: number; + image: string; + imageUrls: string[]; +} + +export interface SearchResults { + results: SearchRecipeItem[]; + baseUri: string; + offset: number; + number: number; + totalResults: number; + processingTimeMs: number; + expires: number; + isStale: boolean; +} + +export interface SearchQuery { + keyword: string; + number: number; + offset: number; +} From d259cf18341e76cccd6e3f97503f684827aa2e92 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Thu, 31 Mar 2022 19:56:20 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20Search=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20CSR=20=EB=A1=9C=EC=A7=81=20=EC=99=84?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RTK 쿼리로 isFetching 상태 시 로딩 컴포넌트 노출 - currentResults에 대해서 data가 바뀔 때 적용, 렌더링하도록 변경 --- src/pages/search/[keyword].tsx | 35 ++++++++++------------------- src/store/services/types/queries.ts | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/pages/search/[keyword].tsx b/src/pages/search/[keyword].tsx index a43849a..e0c4c4a 100644 --- a/src/pages/search/[keyword].tsx +++ b/src/pages/search/[keyword].tsx @@ -1,16 +1,17 @@ -import { Loading, Card, Pagination } from 'components'; +import { Loading, Pagination } from 'components'; import { NextPage } from 'next'; import Head from 'next/head'; import { useEffect, useState } from 'react'; import { useSearchRecipeQuery } from 'store/services'; +import { SearchRecipeItem } from 'store/services/types/queries'; import { ContextProp, SearchPageProps } from './search.types'; const RESULTS_PER_PAGE = 12; const Search: NextPage = ({ keyword, results, totalResults }) => { const [currentPage, setCurrentPage] = useState(1); - const [currentResults, setCurrentResults] = useState([]); - const { data, error, isLoading } = useSearchRecipeQuery({ + const [currentResults, setCurrentResults] = useState([]); + const { data, isFetching } = useSearchRecipeQuery({ keyword, number: RESULTS_PER_PAGE, offset: (currentPage - 1) * RESULTS_PER_PAGE, @@ -18,36 +19,24 @@ const Search: NextPage = ({ keyword, results, totalResults }) = useEffect(() => { if (currentPage !== 1 && data) setCurrentResults(data.results); - }, [currentPage]); + }, [data]); const handleClick = (page: number) => { setCurrentPage(page); }; - return (
    {`Searched: ${keyword}`} - {currentPage !== 1 && isLoading ? ( - - ) : ( -
      - {(currentPage === 1 ? results : currentResults).map(({ id, title, image }) => ( -
    • - -
    • - ))} -
    + {currentPage !== 1 && isFetching && ( + )} +
      + {(currentPage === 1 ? results : currentResults).map(({ id, title, image }) => ( +
    • {title}
    • + ))} +
    Date: Thu, 31 Mar 2022 20:44:50 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20Menu=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20props=20=ED=83=80=EC=9E=85=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 - 타입정의만 하고 prop에 적용을 안해서 적어두었습니다. --- src/components/Menu/Menu.tsx | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index a726b46..b682d4d 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,13 +1,14 @@ import { useState } from 'react'; import { useRouter } from 'next/router'; import { actions } from 'store/slices/auth'; -import { IconButton, Button, Toast } from 'components'; +import { IconButton, Button } from 'components'; import Link from 'next/link'; import { logOut } from 'api/requestAuth'; import { useDispatch } from 'react-redux'; import { StyledNav, StyledUl, StyledLi } from './Menu.styled'; +import { MenuProps } from './Menu.types'; -export const Menu = ({ onSignOut }) => { +export const Menu = ({ onSignOut }: MenuProps) => { const [isOpen, setIsOpen] = useState(false); const dispatch = useDispatch(); const handleClick = () => { @@ -25,22 +26,20 @@ export const Menu = ({ onSignOut }) => { dispatch(actions.signOut()); onSignOut(); }; - const router = useRouter(); - /* - TODO: 스토리북에서 next.js 설정 후 주석 해제 - useEffect(() => { - const handleRouteChange = () => { - setIsOpen(false); - }; + // const router = useRouter(); + + // useEffect(() => { + // const handleRouteChange = () => { + // setIsOpen(false); + // }; - router.events.on('routeChangeStart', handleRouteChange); + // router.events.on('routeChangeStart', handleRouteChange); - return () => { - router.events.off('routeChangeStart', handleRouteChange); - }; - }, []); - */ + // return () => { + // router.events.off('routeChangeStart', handleRouteChange); + // }; + // }, []); return ( From db843e753cf927c5a0a6fc52b0ade1927f875686 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Thu, 31 Mar 2022 20:45:37 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=8B=9C=20auth=EC=83=81=ED=83=9C=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이제 authStatus를 firebase로부터 가져와 추가합니다. --- src/components/Header/Header.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 0398c59..911cf60 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -3,10 +3,11 @@ import { useToast } from 'hooks'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { RootState } from 'store'; -import { AuthState } from 'store/slices/auth'; +import { AuthState, actions } from 'store/slices/auth'; import { HEADER_HEIGHT } from 'styles/GlobalStyle'; +import { getAuthStatus } from 'api/requestAuth'; import { StyledDiv, StyledHeader, StyledIconButton } from './Header.styled'; export const Header = (): JSX.Element => { @@ -17,6 +18,16 @@ export const Header = (): JSX.Element => { const [showScrollToTop, setShowScrollToTop] = useState(false); const oldScrollTop = useRef(0); const { authUser, isLoading } = useSelector((state) => state.auth); + const dispatch = useDispatch(); + + useEffect(() => { + (async () => { + dispatch(actions.loading(true)); + const user = await getAuthStatus(); + if (user) dispatch(actions.signIn(user.uid)); + else dispatch(actions.loading(false)); + })(); + }, []); const handleOpenDialog = () => { setShowDialog(true); From 90498ddb58f3ec39d6db4d02643b219d4294cd20 Mon Sep 17 00:00:00 2001 From: hanana1253 Date: Thu, 31 Mar 2022 21:34:10 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20ErrorBoundary=20redirection=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=9E=91=EB=8F=99=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 - withRouter를 통해 path가 '/'이고 링크를 통해 접근하는 경우 hasError 값을 false로 초기화 --- src/components/ErrorBoundary/ErrorBoundary.tsx | 18 ++++++++++++++++-- .../ErrorBoundary/ErrorBoundary.types.ts | 8 ++++++-- src/pages/_app.tsx | 6 +++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index df1bd58..2428046 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,9 +1,9 @@ import React from 'react'; import Link from 'next/link'; +import { withRouter } from 'next/router'; import { Heading, Header, EmptyPage } from '..'; import { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary.types'; - -export class ErrorBoundary extends React.Component { +class ErrorBoundary extends React.Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { @@ -11,10 +11,22 @@ export class ErrorBoundary extends React.Component - + - + );