diff --git a/src/apis/auth/getGithubLogin.ts b/src/apis/auth/getGithubLogin.ts new file mode 100644 index 00000000..bd415e6d --- /dev/null +++ b/src/apis/auth/getGithubLogin.ts @@ -0,0 +1,21 @@ +import { getGithubLoginPayload, getGithubLoginResponseType } from "api-models" + +import authToken from "@stores/authToken" + +import { ENDPOINTS } from "@constants/endPoints" + +import { baseInstance } from "../axiosInstance" + +export const getGithubLogin = async ({ + code, + state, +}: getGithubLoginPayload) => { + const { data } = await baseInstance.get( + `${ENDPOINTS.GITHUB_LOGIN}?code=${code}&state=${state}`, + ) + + authToken.setAccessToken(data.accessToken) + authToken.setRefreshToken(data.refreshToken) + + return data +} diff --git a/src/apis/auth/postEmailAuth.ts b/src/apis/auth/postEmailAuth.ts index 857f475b..069c65f7 100644 --- a/src/apis/auth/postEmailAuth.ts +++ b/src/apis/auth/postEmailAuth.ts @@ -5,7 +5,6 @@ import { ENDPOINTS } from "@constants/endPoints" import { authInstance } from "../axiosInstance" export const postEmailAuth = async () => { - //TODO: accessToken을 소셜 로그인과 이메일 로그인으로 분리하기위해 접두어로 'email', 'github' 등을 붙여야해서 slice로 잘라내는 작업 필요 const { data } = await authInstance.post( ENDPOINTS.EMAIL_AUTH, ) diff --git a/src/apis/project/getAllProjects.ts b/src/apis/project/getAllProjects.ts index e0227348..ddf90ec7 100644 --- a/src/apis/project/getAllProjects.ts +++ b/src/apis/project/getAllProjects.ts @@ -11,7 +11,14 @@ import { baseInstance } from "../axiosInstance" * @brief 전체 프로젝트 목록을 가져옵니다 */ export const getAllProjects = async ( - { sortOption, isReleased, lastProjectId, lastProject }: getAllProjectsType, + { + sortOption, + isReleased, + lastProjectId, + lastProject, + search, + skill, + }: getAllProjectsType, config: AxiosRequestConfig = { headers: { Authorization: `Bearer ${authToken.getAccessToken()}`, @@ -38,6 +45,8 @@ export const getAllProjects = async ( pageSize: 10, lastProjectId, lastOrderCount, + search, + skill: skill?.join(","), }, }, ) diff --git a/src/components/ErrorBoundary/AuthErrorBoundary/AuthErrorBoundary.tsx b/src/components/ErrorBoundary/AuthErrorBoundary/AuthErrorBoundary.tsx index bee010b0..1d30276b 100644 --- a/src/components/ErrorBoundary/AuthErrorBoundary/AuthErrorBoundary.tsx +++ b/src/components/ErrorBoundary/AuthErrorBoundary/AuthErrorBoundary.tsx @@ -28,14 +28,12 @@ class AuthErrorBoundary extends Component { } static getDerivedStateFromError(error: Error) { - // 에러를 잡아서 state를 업데이트하여 다음 렌더링에서 폴백 UI를 보여줄 수 있게 합니다. if (isAuthError(error)) { return { error } } } componentDidCatch(error: Error) { - // LogoutError를 받으면 로그아웃합니다. if (error instanceof LogoutError) { this.props.logout() } @@ -47,9 +45,6 @@ class AuthErrorBoundary extends Component { } render() { - // Logout이나 Permission 에러가 발생한 경우 모달을 띄웁니다. - console.log(this.state.error) - return ( <> {this.state.error ? ( diff --git a/src/components/ErrorBoundary/ErrorBoundaries.tsx b/src/components/ErrorBoundary/ErrorBoundaries.tsx index 78d75649..84f4e23d 100644 --- a/src/components/ErrorBoundary/ErrorBoundaries.tsx +++ b/src/components/ErrorBoundary/ErrorBoundaries.tsx @@ -5,17 +5,20 @@ import { QueryErrorResetBoundary } from "@tanstack/react-query" import useLogout from "@hooks/useLogout" import AuthErrorBoundary from "./AuthErrorBoundary/AuthErrorBoundary" +import UncaughtErrorBoundary from "./UncaughtErrorBoundary/UncaughtErrorBoundary" const ErrorBoundaries = ({ children }: PropsWithChildren) => { const logout = useLogout() return ( {({ reset }) => ( - - {children} - + + + {children} + + )} ) diff --git a/src/components/ErrorBoundary/UncaughtErrorBoundary/UncaughtErrorBoundary.tsx b/src/components/ErrorBoundary/UncaughtErrorBoundary/UncaughtErrorBoundary.tsx new file mode 100644 index 00000000..1607f812 --- /dev/null +++ b/src/components/ErrorBoundary/UncaughtErrorBoundary/UncaughtErrorBoundary.tsx @@ -0,0 +1,49 @@ +import { Component, PropsWithChildren } from "react" + +import UncaughtFallback from "./components/UncaughtFallback" + +type State = { + error: Error | null +} + +interface UncaughtErrorBoundaryProps extends PropsWithChildren { + reset: () => void +} + +class UncaughtErrorBoundary extends Component< + UncaughtErrorBoundaryProps, + State +> { + constructor(props: UncaughtErrorBoundaryProps) { + super(props) + this.state = { + error: null, + } + } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + handleReset = () => { + this.props.reset() + this.setState({ error: null }) + } + + render() { + return ( + <> + {this.state.error ? ( + + ) : ( + this.props.children + )} + + ) + } +} + +export default UncaughtErrorBoundary diff --git a/src/components/ErrorBoundary/UncaughtErrorBoundary/components/UncaughtFallback.tsx b/src/components/ErrorBoundary/UncaughtErrorBoundary/components/UncaughtFallback.tsx new file mode 100644 index 00000000..dc902e49 --- /dev/null +++ b/src/components/ErrorBoundary/UncaughtErrorBoundary/components/UncaughtFallback.tsx @@ -0,0 +1,42 @@ +import { GrPowerReset } from "react-icons/gr" +import { useNavigate } from "react-router-dom" + +import { Center, Icon, IconButton, Text, VStack } from "@chakra-ui/react" + +interface UncaughtFallbackProps { + error: Error + onReset: () => void +} + +const UncaughtFallback = ({ error, onReset }: UncaughtFallbackProps) => { + const navigate = useNavigate() + return ( +
+ + + 😓 {error.name} + + + } + onClick={() => { + onReset() + navigate("/") + }} + /> + +
+ ) +} + +export default UncaughtFallback diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 692b924f..3619a6cd 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,4 +1,6 @@ +import { useRef } from "react" import { IoSearch } from "react-icons/io5" +import { useNavigate } from "react-router-dom" import { Box, @@ -17,12 +19,21 @@ import LoginButton from "./components/LoginButton" import Menu from "./components/Menu/Menu" const Header = () => { + const navigate = useNavigate() const isLoggedIn = useUserInfoData() + const inputRef = useRef(null) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (inputRef.current) { + navigate(`/project?search=${inputRef.current.value}`) + inputRef.current.value = "" + } + } + return ( - {/* 로고 */} - {/* 검색창 */} { h="2rem" /> - +
+ +
- {/* 메뉴 */} {isLoggedIn ? : } ) diff --git a/src/components/ProjectCard/ProjectCard.tsx b/src/components/ProjectCard/ProjectCard.tsx index d4c7feb2..97e72de2 100644 --- a/src/components/ProjectCard/ProjectCard.tsx +++ b/src/components/ProjectCard/ProjectCard.tsx @@ -1,6 +1,7 @@ import { ForwardedRef, forwardRef } from "react" import { IoMdHeart, IoMdHeartEmpty } from "react-icons/io" import { MdRemoveRedEye } from "react-icons/md" +import { useNavigate } from "react-router-dom" import { Box, @@ -22,6 +23,7 @@ interface ProjectCardProps { isFullHeart: boolean title: string content: string + url: string } const ProjectCard = forwardRef( @@ -33,13 +35,17 @@ const ProjectCard = forwardRef( isFullHeart, title, content, + url, }: ProjectCardProps, ref: ForwardedRef, ) => { + const navigate = useNavigate() + return ( navigate(url)} padding="1rem" cursor="pointer">
void + sortOption: SortSelectType + handleSelect: (e: ChangeEvent) => void +} + +const ProjectFilter = ({ + handleChange, + sortOption, + handleSelect, +}: ProjectFilterProps) => { + return ( + + + + 출시 서비스만 보기 + + + + ) +} + +export default ProjectFilter diff --git a/src/components/ProjectList/ProjectList.tsx b/src/components/ProjectList/ProjectList.tsx index 0e8fc5a8..10679b06 100644 --- a/src/components/ProjectList/ProjectList.tsx +++ b/src/components/ProjectList/ProjectList.tsx @@ -1,7 +1,6 @@ import { ForwardedRef, forwardRef } from "react" -import { Link } from "react-router-dom" -import { Center, Grid, Skeleton } from "@chakra-ui/react" +import { Center, Grid, Spinner, Text } from "@chakra-ui/react" import { getAllProjectsResponseType } from "api-models" import { InfiniteData } from "@tanstack/react-query" @@ -11,41 +10,62 @@ import ProjectCard from "@components/ProjectCard/ProjectCard" interface ProjectListProps { projects: InfiniteData | undefined isLoading: boolean + isFetchingNextPage?: boolean } const ProjectList = forwardRef( ( - { projects, isLoading }: ProjectListProps, + { projects, isLoading, isFetchingNextPage }: ProjectListProps, ref: ForwardedRef, ) => { + const projectCount = + projects != undefined && projects.pages[0].totalElements + return ( - - {projects?.pages.map((projectList) => { - return projectList.content.map((project) => ( -
- - - - - -
- )) - })} -
+ <> + {isLoading ? ( +
+ +
+ ) : ( + + {!projectCount && !isLoading ? ( +
+ 프로젝트가 없습니다 +
+ ) : ( + projects?.pages.map((projectList) => { + return projectList.content.map((project) => ( +
+ +
+ )) + }) + )} + {isFetchingNextPage ? : null} +
+ )} + ) }, ) diff --git a/src/constants/endPoints.ts b/src/constants/endPoints.ts index b6bd3bf3..1925a6e2 100644 --- a/src/constants/endPoints.ts +++ b/src/constants/endPoints.ts @@ -1,7 +1,7 @@ -const VARIABLE_URL = "/api/v1" +export const VARIABLE_URL = "/api/v1" export const ENDPOINTS = { - GITHUB_LOGIN: `${VARIABLE_URL}/auth/login/github`, + GITHUB_LOGIN: `${VARIABLE_URL}/login/oauth2/code/github`, EMAIL_LOGIN: `${VARIABLE_URL}/auth/login`, EMAIL_REFRESH: `${VARIABLE_URL}/auth/reissue`, EMAIL_AUTH: `${VARIABLE_URL}/auth/me`, diff --git a/src/constants/externalLinks.ts b/src/constants/externalLinks.ts new file mode 100644 index 00000000..694199b7 --- /dev/null +++ b/src/constants/externalLinks.ts @@ -0,0 +1,5 @@ +import { VARIABLE_URL } from "./endPoints" + +export const EXTERNAL_LINKS = { + GITHUB_LOGIN: `${VARIABLE_URL}/oauth2/authorization/github`, +} diff --git a/src/constants/queryKey.ts b/src/constants/queryKey.ts index 63b5a959..5b8e65b6 100644 --- a/src/constants/queryKey.ts +++ b/src/constants/queryKey.ts @@ -11,4 +11,5 @@ export const QUERYKEY = { POST_COMMENT: "postComment", EDIT_COMMENT: "editComment", DELETE_COMMENT: "deleteComment", + GITHUB_LOGIN: "githubLogin", } diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 22018536..c04a2947 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -1,11 +1,10 @@ -import { Box, Skeleton } from "@chakra-ui/react" +import { Skeleton } from "@chakra-ui/react" import Banner from "./components/Banner/Banner" import ProjectListSection from "./components/ProjectListSection/ProjectListSection" import { useBannerProjectQuery } from "./hooks/queries/useBannerProjectQuery" const HomePage = () => { - // 배너 프로젝트 조회 const { bannerProjectList, isBannerLoading } = useBannerProjectQuery() return ( @@ -15,8 +14,7 @@ const HomePage = () => { ) : ( )} - - + ) } diff --git a/src/pages/HomePage/components/Banner/Banner.tsx b/src/pages/HomePage/components/Banner/Banner.tsx index 31a612e4..867f8f96 100644 --- a/src/pages/HomePage/components/Banner/Banner.tsx +++ b/src/pages/HomePage/components/Banner/Banner.tsx @@ -19,6 +19,7 @@ import "swiper/css/pagination" import { Autoplay, Navigation, Pagination } from "swiper/modules" import { SwiperSlide } from "swiper/react" +import noImage from "@assets/images/noImage.jpg" import sidepeekBlue from "@assets/images/sidepeek_blue.png" import { CustomSwiper } from "./Banner.style" @@ -72,6 +73,7 @@ const Banner = ({ bannerList }: bannerListProps) => { projectImg void hasNext: boolean + isLoading: boolean } -const MoreButton = ({ loadMore, hasNext }: MoreButton) => { +const MoreButton = ({ loadMore, hasNext, isLoading }: MoreButton) => { return ( <> {hasNext && (
- + {isLoading ? ( + + ) : ( + + )}
)} diff --git a/src/pages/HomePage/components/ProjectListSection/ProjectListSection.tsx b/src/pages/HomePage/components/ProjectListSection/ProjectListSection.tsx index d13238ae..e52bd6b3 100644 --- a/src/pages/HomePage/components/ProjectListSection/ProjectListSection.tsx +++ b/src/pages/HomePage/components/ProjectListSection/ProjectListSection.tsx @@ -1,18 +1,10 @@ -import { ChangeEvent, useCallback, useEffect, useState } from "react" -import { useInView } from "react-intersection-observer" +import { ChangeEvent, useCallback, useState } from "react" -import { - Checkbox, - Container, - HStack, - Select, - Spacer, - Stack, - useMediaQuery, -} from "@chakra-ui/react" +import { Container, Stack, useMediaQuery } from "@chakra-ui/react" import { useQueryClient } from "@tanstack/react-query" +import ProjectFilter from "@components/ProjectFilter/ProjectFilter" import ProjectList from "@components/ProjectList/ProjectList" import { useAllProjectQuery } from "@pages/HomePage/hooks/queries/useAllProjectQuery" @@ -22,23 +14,15 @@ import { QUERYKEY } from "@constants/queryKey" import MoreButton from "../MoreButton/MoreButton" -interface ProjectListSectionProps { - isInfinityScroll?: boolean -} - -const ProjectListSection = ({ - isInfinityScroll = false, -}: ProjectListSectionProps) => { +const ProjectListSection = () => { const [isLargerThan1200] = useMediaQuery("(min-width: 1200px)") const [isReleased, setIsReleased] = useState(false) const [sortOption, setSortOption] = useState("createdAt") const queryClient = useQueryClient() - // 프로젝트 전체 목록 조회 const { allProjectList, isAllProjectLoading, - refetchAllProject, fetchNextPage, hasNextPage, isFetchingNextPage, @@ -50,14 +34,17 @@ const ProjectListSection = ({ const handleSelect = (e: ChangeEvent) => { const value = e.target.value as SortSelectType - // 다른 정렬 옵션 선택시 초기화 후 리패치 if (value !== sortOption) { + setSortOption(value) queryClient.removeQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) queryClient.refetchQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) } - setSortOption(value) + } - refetchAllProject() + const handleChange = () => { + setIsReleased(!isReleased) + queryClient.removeQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) + queryClient.refetchQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) } const loadMoreProjects = useCallback(() => { @@ -66,50 +53,24 @@ const ProjectListSection = ({ } }, [hasNextPage, isFetchingNextPage, fetchNextPage]) - const { ref, inView } = useInView({ threshold: 0 }) - - useEffect(() => { - if (isInfinityScroll && inView) { - fetchNextPage() - } - }) - return ( - - - { - setIsReleased(!isReleased) - refetchAllProject() - }}> - 출시 서비스만 보기 - - - + - {!isInfinityScroll && ( - - )} + ) } diff --git a/src/pages/HomePage/hooks/queries/useAllProjectQuery.ts b/src/pages/HomePage/hooks/queries/useAllProjectQuery.ts index 7aae3ac8..bae2a5d2 100644 --- a/src/pages/HomePage/hooks/queries/useAllProjectQuery.ts +++ b/src/pages/HomePage/hooks/queries/useAllProjectQuery.ts @@ -11,6 +11,8 @@ export const useAllProjectQuery = ({ isReleased, lastProjectId = null, lastProject = undefined, + search, + skill, }: getAllProjectsType) => { const { data, @@ -27,6 +29,8 @@ export const useAllProjectQuery = ({ isReleased, lastProjectId, lastProject, + search, + skill, ], queryFn: () => getAllProjects({ @@ -34,6 +38,8 @@ export const useAllProjectQuery = ({ isReleased, lastProjectId, lastProject, + search, + skill, }), initialPageParam: 0, getNextPageParam: (lastPage) => ( diff --git a/src/pages/LoginPage/components/SocialLogin.tsx b/src/pages/LoginPage/components/SocialLogin.tsx index 9dc9489b..53575d24 100644 --- a/src/pages/LoginPage/components/SocialLogin.tsx +++ b/src/pages/LoginPage/components/SocialLogin.tsx @@ -1,6 +1,8 @@ import { FaGithub } from "react-icons/fa" -import { Box, Button, Heading, Icon } from "@chakra-ui/react" +import { Box, Button, Heading, Icon, Link } from "@chakra-ui/react" + +import { EXTERNAL_LINKS } from "@constants/externalLinks" const SocialLogin = () => { return ( @@ -14,6 +16,8 @@ const SocialLogin = () => {
+ + + + ) +} + +export default AlertModal diff --git a/src/pages/NicknameSetupPage/components/NicknameInput.tsx b/src/pages/NicknameSetupPage/components/NicknameInput.tsx new file mode 100644 index 00000000..2a28fe75 --- /dev/null +++ b/src/pages/NicknameSetupPage/components/NicknameInput.tsx @@ -0,0 +1,22 @@ +import InputController from "@components/InputController/InputController" + +import InputWithDoubleCheck from "@pages/SignUpPage/components/SignUpForm/components/InputWithDoubleCheck" +import { nicknameOptions } from "@pages/SignUpPage/constants/registerOptions" + +const NicknameInput = () => { + return ( + + {(renderProps) => ( + + )} + + ) +} + +export default NicknameInput diff --git a/src/pages/NicknameSetupPage/components/SubmitButton.tsx b/src/pages/NicknameSetupPage/components/SubmitButton.tsx new file mode 100644 index 00000000..5da6a82b --- /dev/null +++ b/src/pages/NicknameSetupPage/components/SubmitButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps } from "@chakra-ui/react" + +const SubmitButton = (props: ButtonProps) => { + return ( + + ) +} + +export default SubmitButton diff --git a/src/pages/NicknameSetupPage/constants/toastMessages.ts b/src/pages/NicknameSetupPage/constants/toastMessages.ts new file mode 100644 index 00000000..a4609778 --- /dev/null +++ b/src/pages/NicknameSetupPage/constants/toastMessages.ts @@ -0,0 +1,3 @@ +export const NICKNAME_SETUP_MESSAGE = { + ERROR: "닉네임을 확인해주세요.", +} diff --git a/src/pages/NicknameSetupPage/hooks/query/useGithubLoginQuery.ts b/src/pages/NicknameSetupPage/hooks/query/useGithubLoginQuery.ts new file mode 100644 index 00000000..a55b0f39 --- /dev/null +++ b/src/pages/NicknameSetupPage/hooks/query/useGithubLoginQuery.ts @@ -0,0 +1,17 @@ +import { useSuspenseQuery } from "@tanstack/react-query" + +import { getGithubLogin } from "@apis/auth/getGithubLogin" + +import { QUERYKEY } from "@constants/queryKey" + +interface UseGithubLoginQuery { + code: string + state: string +} + +export const useGithubLoginQuery = (body: UseGithubLoginQuery) => { + return useSuspenseQuery({ + queryKey: [QUERYKEY.GITHUB_LOGIN, body], + queryFn: () => getGithubLogin(body), + }) +} diff --git a/src/pages/NicknameSetupPage/hooks/useNicknameSetup.ts b/src/pages/NicknameSetupPage/hooks/useNicknameSetup.ts new file mode 100644 index 00000000..2d01f3d9 --- /dev/null +++ b/src/pages/NicknameSetupPage/hooks/useNicknameSetup.ts @@ -0,0 +1,92 @@ +import { SubmitHandler, useForm } from "react-hook-form" +import { useNavigate, useSearchParams } from "react-router-dom" + +import { useToast } from "@chakra-ui/react" +import { getUserDetailResponseType } from "api-models" + +import { useQueryClient } from "@tanstack/react-query" + +import { authInstance } from "@apis/axiosInstance" + +import { LOGIN_MESSAGES } from "@pages/LoginPage/constants/toastMessages" +import { requiredDoubleCheckErrors } from "@pages/SignUpPage/constants/errorOptions" +import { toastOptions } from "@pages/SignUpPage/constants/toastOptions" + +import { useDoubleCheckStore } from "@stores/useDoubleCheckStore" + +import { ENDPOINTS } from "@constants/endPoints" +import { QUERYKEY } from "@constants/queryKey" + +import { NICKNAME_SETUP_MESSAGE } from "../constants/toastMessages" +import { useGithubLoginQuery } from "./query/useGithubLoginQuery" + +interface NicknameFormType { + nickname: string +} + +export const useNicknameSetup = () => { + const toast = useToast(toastOptions) + const navigate = useNavigate() + const searchParams = useSearchParams()[0] + + const queryClient = useQueryClient() + + const { + data: { user: userInfo }, + isError, + } = useGithubLoginQuery({ + code: searchParams.get("code") ?? "", + state: searchParams.get("state") ?? "", + }) + + const method = useForm() + + const checkedNickname = useDoubleCheckStore((state) => state.nickname) + + const onLoginSuccess = () => { + queryClient.setQueryData([QUERYKEY.USER_INFO], userInfo) + toast({ + status: "success", + title: LOGIN_MESSAGES.SUCCESS, + }) + navigate("/") + } + + const onSubmit: SubmitHandler = async ({ nickname }) => { + if (nickname !== checkedNickname) { + method.setError("nickname", requiredDoubleCheckErrors.nickname) + return + } + + if (userInfo.id === null) { + return + } + + try { + const profile = await authInstance.get( + ENDPOINTS.GET_USER_PROFILE(userInfo.id), + ) + await authInstance.put(ENDPOINTS.PUT_USER_PROFILE(userInfo.id), { + ...profile.data, + nickname, + }) + onLoginSuccess() + } catch (error) { + toast({ + status: "error", + title: NICKNAME_SETUP_MESSAGE.ERROR, + }) + method.setError("nickname", { + type: "validate", + }) + } + } + + return { + onSubmit, + method, + isError, + onLoginSuccess, + isNicknameSet: userInfo.nickname !== null, + } +} diff --git a/src/pages/ProfilePage/components/Projects/ProjectsGrid.tsx b/src/pages/ProfilePage/components/Projects/ProjectsGrid.tsx index ef6038b0..1a935621 100644 --- a/src/pages/ProfilePage/components/Projects/ProjectsGrid.tsx +++ b/src/pages/ProfilePage/components/Projects/ProjectsGrid.tsx @@ -52,6 +52,7 @@ const ProjectsGrid = ({ userId, type }: ProjectsGridProps) => { }: ProjectProperties) => ( { - const params = new URLSearchParams(window.location.search) - const search = params.get("search") + const params = useSearchParams()[0] + const keyword = params.get("search") + + const [search, setSearch] = useState(keyword) + const navigate = useNavigate() + + const [isLargerThan1200] = useMediaQuery("(min-width: 1200px)") + + const [isReleased, setIsReleased] = useState(false) + const [sortOption, setSortOption] = useState("createdAt") + const queryClient = useQueryClient() + const { ref, inView } = useInView({ threshold: 0 }) + + const { + allProjectList, + isAllProjectLoading, + refetchAllProject, + fetchNextPage, + isRefetching, + isFetchingNextPage, + } = useAllProjectQuery({ sortOption, isReleased, search }) + + const isLoading = isAllProjectLoading || isRefetching + + const projectCount = + allProjectList != undefined && allProjectList.pages[0].totalElements + + const handleSelect = (e: ChangeEvent) => { + const value = e.target.value as SortSelectType + + if (value !== sortOption) { + setSortOption(value) + queryClient.removeQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) + queryClient.refetchQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) + } + } + + const handleChange = () => { + setIsReleased(!isReleased) + queryClient.removeQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) + queryClient.refetchQueries({ queryKey: [QUERYKEY.ALL_PROJECTS] }) + } + + useEffect(() => { + if (inView) { + fetchNextPage() + } + }) + + const handleSearch = (keyword: string) => { + setSearch(keyword) + navigate(`/project?search=${keyword}`) + } + + const location = useLocation() + + useEffect(() => { + refetchAllProject() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location]) return ( <> - + - - + + + {projectCount ? ( + + ) : null} + + + ) } diff --git a/src/pages/ProjectListPage/components/ResultInfo.tsx b/src/pages/ProjectListPage/components/ResultInfo.tsx deleted file mode 100644 index e5338871..00000000 --- a/src/pages/ProjectListPage/components/ResultInfo.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Heading, Stack, Text } from "@chakra-ui/react" - -interface ResultInfoProps { - searchWord: string - resultCount: number -} - -const ResultInfo = ({ searchWord, resultCount }: ResultInfoProps) => { - return ( - - '{searchWord}' 검색결과 - {resultCount}개의 프로젝트를 발견하였습니다 - - ) -} - -export default ResultInfo diff --git a/src/pages/ProjectListPage/components/ResultInfo/ResultInfo.tsx b/src/pages/ProjectListPage/components/ResultInfo/ResultInfo.tsx new file mode 100644 index 00000000..a90f696a --- /dev/null +++ b/src/pages/ProjectListPage/components/ResultInfo/ResultInfo.tsx @@ -0,0 +1,27 @@ +import { Heading, Spinner, Stack, Text } from "@chakra-ui/react" + +interface ResultInfoProps { + searchWord: string + resultCount: number | null + isLoading: boolean +} + +const ResultInfo = ({ + searchWord, + resultCount, + isLoading, +}: ResultInfoProps) => { + return ( + + '{searchWord}' 검색결과 + + {isLoading ? : resultCount}개의 프로젝트를 + 발견하였습니다 + + + ) +} + +export default ResultInfo diff --git a/src/pages/ProjectListPage/components/SearchSection.tsx b/src/pages/ProjectListPage/components/SearchBarSection/SearchBarSection.tsx similarity index 65% rename from src/pages/ProjectListPage/components/SearchSection.tsx rename to src/pages/ProjectListPage/components/SearchBarSection/SearchBarSection.tsx index 007562eb..9e493ead 100644 --- a/src/pages/ProjectListPage/components/SearchSection.tsx +++ b/src/pages/ProjectListPage/components/SearchBarSection/SearchBarSection.tsx @@ -1,13 +1,32 @@ -import { useEffect, useRef } from "react" +import { useEffect, useRef, useState } from "react" import { IoMdSearch } from "react-icons/io" import { Box, Button, Center, Icon, useMediaQuery } from "@chakra-ui/react" import CommonInput from "@components/Input/CommonInput" -const SearchSection = () => { +interface SearchBarSectionProps { + search: string + onSubmit: (keyword: string) => void +} + +const SearchBarSection = ({ search, onSubmit }: SearchBarSectionProps) => { const [isLargerThan768] = useMediaQuery("(min-width: 768px)") const $ref = useRef(null) + const [inputValue, setInputValue] = useState(search) + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + if (!inputValue) { + alert("검색어를 입력하세요") + } + + onSubmit(inputValue) + } + + const handleChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value) + } useEffect(() => { $ref.current?.focus() @@ -22,7 +41,7 @@ const SearchSection = () => { left="50%" top="20rem" transform="translate(-50%,-50%)"> -
console.log("enter")}> + { height="7rem" fontSize="2xl" border="0" + marginLeft="1rem" + value={inputValue} + onChange={handleChange} />