diff --git a/pages/profile/index.tsx b/pages/profile/index.tsx index 94439d0..bb6ebd8 100644 --- a/pages/profile/index.tsx +++ b/pages/profile/index.tsx @@ -1,4 +1,5 @@ import BackgroundImage from '@components/common/BackgroundImage'; +import { DefaultButton } from '@components/common/DefaultButton'; import { DefaultHeader } from '@components/common/DefaultHeader'; import { HeaderBackButton } from '@components/common/HeaderBackButton'; import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; @@ -10,6 +11,7 @@ import ProfileTab from '@components/profile/ProfileTab'; import styled from '@emotion/styled'; import { GetServerSideProps } from 'next'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import { useRef } from 'react'; import { useScroll } from 'react-use'; @@ -28,18 +30,27 @@ const Container = styled.div` const ProfileInfoContainer = styled.div` display: flex; flex-direction: column; - height: 200px; width: 100%; `; const RightAreaContainer = styled.div` display: flex; height: 100%; + justify-content: flex-end; padding: 0 8px; align-items: center; cursor: pointer; `; +const ProfileLoginButtonWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + height: 120px; + padding: 0 30px; +`; + const RightArea = () => { return ( @@ -51,6 +62,10 @@ const RightArea = () => { const Profile = () => { const scrollRef = useRef(null); const { y } = useScroll(scrollRef); + const { push } = useRouter(); + const onClickLoginButton = () => { + push('/signin'); + }; return ( @@ -58,7 +73,18 @@ const Profile = () => { { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} suspenseFallback={} > @@ -79,7 +105,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, - destination: '/profile?category=situation&role=HOST', + destination: '/profile?category=situation&role=VOLUNTEER&status=RECRUIT', }, }; } diff --git a/src/api/getPartyCurrentSituation.ts b/src/api/getPartyCurrentSituation.ts new file mode 100644 index 0000000..b3cfec4 --- /dev/null +++ b/src/api/getPartyCurrentSituation.ts @@ -0,0 +1,24 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { GetPartyCurrentSituationResponse } from 'types/party'; + +type GetPartyCurrentSituationRequestRole = 'HOST' | 'VOLUNTEER'; +export type GetPartyCurrentSituationRequestStatus = 'RECRUIT' | 'RECRUIT_FINISH' | 'PARTY_FINISH'; +interface GetPartyCurrentSituationParameter { + page: number; + size: number; + role: GetPartyCurrentSituationRequestRole; + status: GetPartyCurrentSituationRequestStatus; +} + +export const API_GET_PARTY_STATUS_KEY = '/api/party/party-status'; + +const getPartyStatus = async (params: GetPartyCurrentSituationParameter) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'partyList', GetPartyCurrentSituationResponse> + >(API_GET_PARTY_STATUS_KEY, { + params, + }); + return data; +}; + +export default getPartyStatus; diff --git a/src/api/getPartyJoin.ts b/src/api/getPartyJoin.ts index 4ebce7b..4364b7f 100644 --- a/src/api/getPartyJoin.ts +++ b/src/api/getPartyJoin.ts @@ -1,20 +1,23 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { PartyJoinResponse } from "types/party/join/PartyJoinResponse"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { PartyJoinResponse } from 'types/party/join/PartyJoinResponse'; -interface getPartyJoinParameter { - role: string; +type GetPartyJoinRequestRole = 'HOST' | 'VOLUNTEER'; +interface GetPartyJoinParameter { + page: number; + size: number; + role: GetPartyJoinRequestRole; } -export const API_GET_PARTY_JOIN_KEY = "/api/party/party-join?role={{role}}"; +export const API_GET_PARTY_JOIN_KEY = '/api/party/party-join'; -const getPartyJoin = async ({ - role, -}: getPartyJoinParameter): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_PARTY_JOIN_KEY, { role: role }) - ); - return data; +const getPartyJoin = async (params: GetPartyJoinParameter) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'partyList', PartyJoinResponse> + >(API_GET_PARTY_JOIN_KEY, { + params, + }); + return data; }; export default getPartyJoin; diff --git a/src/api/getPartyMainPage.ts b/src/api/getPartyMainPage.ts index c735b61..65d4830 100644 --- a/src/api/getPartyMainPage.ts +++ b/src/api/getPartyMainPage.ts @@ -1,28 +1,23 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { PartyListResponse } from "types/common/PartyListResponse"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { PartyListResponse } from 'types/common/PartyListResponse'; interface GetMainPageParameter { - longitude: number; - latitude: number; - lastPartyId?: number; - size?: number; + longitude: number; + latitude: number; + page?: number; + size?: number; } -export const API_GET_MAIN_PAGE = - "/api/main"; +export const API_GET_MAIN_PAGE = '/api/main'; const getMainPageData = async (params: GetMainPageParameter) => { - const { data } = await defaultRequest.get< - InfinitePaginationDataType<"partyList", PartyListResponse> - >( - API_GET_MAIN_PAGE, { - params:{ - ...params - } - } - ); - return data; + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'partyList', PartyListResponse> + >(API_GET_MAIN_PAGE, { + params, + }); + return data; }; export default getMainPageData; diff --git a/src/api/getPartyStatus.ts b/src/api/getPartyStatus.ts deleted file mode 100644 index 2bb6691..0000000 --- a/src/api/getPartyStatus.ts +++ /dev/null @@ -1,20 +0,0 @@ -import defaultRequest from "src/lib/axios/defaultRequest"; -import variableAssignMent from "@utils/variableAssignment"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; - -interface getPartyStatusParameter { - role: string; -} - -export const API_GET_PARTY_STATUS_KEY = "/api/party/party-status?role={{role}}"; - -const getPartyStatus = async ({ - role, -}: getPartyStatusParameter): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_PARTY_STATUS_KEY, { role: role }) - ); - return data; -}; - -export default getPartyStatus; diff --git a/src/api/getProfileReviewList.ts b/src/api/getProfileReviewList.ts deleted file mode 100644 index 9784aad..0000000 --- a/src/api/getProfileReviewList.ts +++ /dev/null @@ -1,21 +0,0 @@ -import defaultRequest from 'src/lib/axios/defaultRequest'; -import { GetReviewListResponse } from 'types/review'; - -export type ProfileReviewListRequestType = 'SENDER' | 'RECEIVER'; - -interface GetProfileReviewListParameter { - reviewType: ProfileReviewListRequestType; -} - -export const API_GET_REVIEW_LIST_KEY = '/api/review'; - -const getProfileReviewList = async ({ reviewType }: GetProfileReviewListParameter) => { - const { data } = await defaultRequest.get(API_GET_REVIEW_LIST_KEY, { - params: { - reviewType, - }, - }); - return data; -}; - -export default getProfileReviewList; diff --git a/src/api/getReviewList.ts b/src/api/getReviewList.ts new file mode 100644 index 0000000..7f0f729 --- /dev/null +++ b/src/api/getReviewList.ts @@ -0,0 +1,23 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { GetReviewListResponse } from 'types/review'; + +export type ReviewListRequestType = 'SENDER' | 'RECEIVER'; + +interface GetReviewListParameter { + reviewType: ReviewListRequestType; + page: number; + size: number; +} + +export const API_GET_REVIEW_LIST_KEY = '/api/review'; + +const getReviewList = async (params: GetReviewListParameter) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'reviewGetResList', GetReviewListResponse> + >(API_GET_REVIEW_LIST_KEY, { + params, + }); + return data; +}; + +export default getReviewList; diff --git a/src/components/common/card/ReviewCard.tsx b/src/components/common/card/ReviewCard.tsx index 20b2212..1684379 100644 --- a/src/components/common/card/ReviewCard.tsx +++ b/src/components/common/card/ReviewCard.tsx @@ -8,7 +8,7 @@ import styled from '@emotion/styled'; import dayjs from 'dayjs'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import React, { FC, MouseEventHandler, useCallback, useState } from 'react'; +import React, { FC, MouseEventHandler, useCallback, useMemo, useState } from 'react'; import { GetReviewListResponse } from 'types/review'; interface ReviewCardProps { @@ -87,6 +87,10 @@ const ReviewCard: FC = ({ data, onClickEditButton, onClickDelet push(`/review/${data.reviewId}`); }; + const formattedImage = useMemo(() => { + return data.reviewImg.map((image) => ({ id: crypto.randomUUID(), imageUrl: image })); + }, [data.reviewImg]); + return ( @@ -137,9 +141,9 @@ const ReviewCard: FC = ({ data, onClickEditButton, onClickDelet - {data.reviewImg.length > 0 ? ( + {formattedImage.length > 0 ? ( - {data.reviewImg.map((reviewImage, index) => { + {formattedImage.map((reviewImage, index) => { const handler = () => { onClickImage(index); }; @@ -173,7 +177,7 @@ const ReviewCard: FC = ({ data, onClickEditButton, onClickDelet diff --git a/src/components/hoc/QuerySuspenseErrorBoundary.tsx b/src/components/hoc/QuerySuspenseErrorBoundary.tsx index ee2cb12..11a93ce 100644 --- a/src/components/hoc/QuerySuspenseErrorBoundary.tsx +++ b/src/components/hoc/QuerySuspenseErrorBoundary.tsx @@ -1,41 +1,48 @@ -import DefaultError from "@components/common/DefaultError"; -import DefaultLoading from "@components/common/DefaultLoading"; -import { Suspense } from "@suspensive/react"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; // (*) -import { FC, PropsWithChildren, ReactEventHandler } from "react"; -import { ErrorBoundary } from "react-error-boundary"; +import DefaultError from '@components/common/DefaultError'; +import DefaultLoading from '@components/common/DefaultLoading'; +import { Suspense } from '@suspensive/react'; +import { QueryErrorResetBoundary } from '@tanstack/react-query'; // (*) +import { AxiosError } from 'axios'; +import { FC, PropsWithChildren, ReactEventHandler } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; interface QuerySuspenseErrorBoundaryProps { - children: React.ReactNode; - suspenseFallback?: React.ReactNode | string; - errorFallback?: ( - resetErrorBoundary: ReactEventHandler - ) => React.ReactNode; + children: React.ReactNode; + suspenseFallback?: React.ReactNode | string; + errorFallback?: ({ + resetErrorBoundary, + error, + }: { + resetErrorBoundary: ReactEventHandler; + error?: AxiosError; + }) => React.ReactNode; } -const QuerySuspenseErrorBoundary: FC< - PropsWithChildren -> = ({ children, suspenseFallback, errorFallback }) => { - return ( - - {({ reset }) => ( - - errorFallback ? ( - errorFallback(resetErrorBoundary) - ) : ( - - ) - } - > - }> - {children} - - - )} - - ); +const QuerySuspenseErrorBoundary: FC> = ({ + children, + suspenseFallback, + errorFallback, +}) => { + return ( + + {({ reset }) => ( + + errorFallback ? ( + errorFallback({ resetErrorBoundary, error }) + ) : ( + + ) + } + > + }> + {children} + + + )} + + ); }; export default QuerySuspenseErrorBoundary; diff --git a/src/components/home/HomeList.tsx b/src/components/home/HomeList.tsx index e82afd7..f284876 100644 --- a/src/components/home/HomeList.tsx +++ b/src/components/home/HomeList.tsx @@ -1,6 +1,5 @@ import { DefaultText } from '@components/common/DefaultText'; import NoResult from '@components/common/NoResult'; -import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; import styled from '@emotion/styled'; import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; @@ -10,6 +9,7 @@ import getMainPageData, { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; import { PositionSate } from 'src/recoil-states/positionStates'; import { Color } from 'styles/Color'; import { PartyCard } from './PartyCard'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; const Container = styled.div` display: flex; @@ -36,12 +36,17 @@ export const HomeList: FC = () => { ? getMainPageData({ latitude: position.coords.x, longitude: position.coords.y, - lastPartyId: pageParam, + page: pageParam, size: 5, }) : Promise.resolve(null), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.lastPartyId, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, }); const onClickPartyCard = (id: number) => { diff --git a/src/components/profile/PartyList.tsx b/src/components/profile/PartyList.tsx deleted file mode 100644 index 3d53bb2..0000000 --- a/src/components/profile/PartyList.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import { DefaultText } from "@components/common/DefaultText"; -import Image from "next/image"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; -import Link from "next/link"; -import dayjs from "dayjs"; -interface PartyListProps { - data: PartyDetailResponse; -} - -const Container = styled.div` - display: flex; - flex-direction: row; - gap: 16px; - padding: 16px; - border-bottom: 1px solid #ebebeb; - transition: all 0.1s; - &:hover { - background-color: #dddddd; - } - cursor: pointer; -`; - -const PartyDetail = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - padding: 8px; -`; -const Title = styled.div` - display: flex; - flex-direction: row; -`; -const Address = styled.div` - display: flex; - flex-direction: row; - gap: 4px; -`; -const Time = styled.div` - display: flex; - flex-direction: row; - gap: 4px; -`; - -const PartyList = ({ data }: PartyListProps) => { - const { partyId, partyTitle, partyTime, address, thumbnail } = data; - - return ( - - - 프로필사진 - - - <DefaultText text={partyTitle} size={16} weight={500} /> - -
- -
- -
-
- - ); -}; - -export default PartyList; diff --git a/src/components/profile/PartyRequestItemList.tsx b/src/components/profile/PartyRequestItemList.tsx deleted file mode 100644 index 27ddef4..0000000 --- a/src/components/profile/PartyRequestItemList.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { FC } from 'react'; -import getPartyJoin, { API_GET_PARTY_JOIN_KEY } from 'src/api/getPartyJoin'; -import { PartyRequestRole } from './PartyRequest'; -import PartyRequestList from './PartyRequestCard'; -import { DefaultText } from '@components/common/DefaultText'; -import styled from '@emotion/styled'; -import PartyRequestCard from './PartyRequestCard'; - -interface PartyRequestItemListProps { - role: PartyRequestRole; -} - -const Container = styled.div` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - height: 200px; -`; - -const PartyRequestItemList: FC = ({ role }) => { - const requestList = useSuspenseQuery({ - queryKey: [API_GET_PARTY_JOIN_KEY, { role }], - queryFn: () => getPartyJoin({ role }), - }); - - if (!requestList.data.length) { - return ( - - - - ); - } - - return requestList.data.map((request) => ( - - )); -}; - -export default PartyRequestItemList; diff --git a/src/components/profile/PartySituation.tsx b/src/components/profile/PartySituation.tsx deleted file mode 100644 index 91381e7..0000000 --- a/src/components/profile/PartySituation.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; -import styled from '@emotion/styled'; -import { useMemo, useState } from 'react'; -import PartySituationItemList from './PartySituationItemList'; -import ProfileTabSortingButton from './ProfileTabSortingButton'; -import { useRouter } from 'next/router'; -import { useSearchParam } from 'react-use'; - -type PartySituationType = '모집중' | '참가중'; -export type PartySituationRole = 'HOST' | 'VOLUNTEER'; - -interface CategoryItemType { - id: PartySituationRole; - label: PartySituationType; -} - -const Container = styled.div` - display: flex; - flex-direction: column; -`; -const PartyListContainer = styled.div` - display: flex; - flex-direction: column; - gap: 12px; - padding: 0 16px; - overflow: auto; -`; -const TabWrapper = styled.div` - display: flex; - gap: 10px; - padding: 10px; -`; - -const categoryTab: CategoryItemType[] = [ - { id: 'HOST', label: '모집중' }, - { id: 'VOLUNTEER', label: '참가중' }, -]; - -function isPartySituationRole(value: unknown): value is PartySituationRole { - return value === 'HOST' || value === 'VOLUNTEER'; -} - -const PartySituation = () => { - // const [selectedRole, setSelectedRole] = useState('HOST'); - const { replace, query } = useRouter(); - const situationRole = useSearchParam('role'); - const selectedRole = useMemo(() => { - if (!situationRole || !isPartySituationRole(situationRole)) { - return; - } - return situationRole; - }, [situationRole]); - - return ( - - - {categoryTab.map((tab) => { - const onClick = () => { - replace({ query: { ...query, role: tab.id } }); - }; - - return ( - - ); - })} - - - - - - - - - ); -}; - -export default PartySituation; diff --git a/src/components/profile/PartySituationItemList.tsx b/src/components/profile/PartySituationItemList.tsx deleted file mode 100644 index e82dbcd..0000000 --- a/src/components/profile/PartySituationItemList.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { FC } from 'react'; -import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyStatus'; -import { PartySituationRole } from './PartySituation'; -import PartyList from './PartyList'; -import { DefaultText } from '@components/common/DefaultText'; -import styled from '@emotion/styled'; - -interface PartySituationItemListProps { - selectedRole: PartySituationRole; -} - -const Container = styled.div` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - height: 200px; -`; - -const PartySituationItemList: FC = ({ selectedRole }) => { - const statusList = useSuspenseQuery({ - queryKey: [API_GET_PARTY_STATUS_KEY, { role: selectedRole }], - queryFn: () => getPartyStatus({ role: selectedRole }), - }); - if (!statusList.data.length) { - return ( - - - - ); - } - return statusList.data.map((status) => ); -}; - -export default PartySituationItemList; diff --git a/src/components/profile/ProfileInfo.tsx b/src/components/profile/ProfileInfo.tsx index 29a170d..2c85610 100644 --- a/src/components/profile/ProfileInfo.tsx +++ b/src/components/profile/ProfileInfo.tsx @@ -1,117 +1,114 @@ -import React from "react"; -import styled from "@emotion/styled"; -import Progressbar from "@components/common/ProgressBar"; -import Image from "next/image"; -import { DefaultText } from "@components/common/DefaultText"; -import GenderIcon from "@components/icons/profile/Gender.icon"; -import InfoIcon from "@components/icons/profile/Info.icon"; -import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { API_GET_PROFILE_KEY } from "src/api/getProfile"; -import getProfile from "src/api/getProfile"; -import { PARTY_GENDER_LABEL } from "src/constants/options"; -import { labelDataConvert } from "@utils/labelDataConvert"; +import React, { useMemo } from 'react'; +import styled from '@emotion/styled'; +import Progressbar from '@components/common/ProgressBar'; +import Image from 'next/image'; +import { DefaultText } from '@components/common/DefaultText'; +import GenderIcon from '@components/icons/profile/Gender.icon'; +import InfoIcon from '@components/icons/profile/Info.icon'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { API_GET_PROFILE_KEY } from 'src/api/getProfile'; +import getProfile from 'src/api/getProfile'; +import { PARTY_GENDER_LABEL } from 'src/constants/options'; +import { labelDataConvert } from '@utils/labelDataConvert'; const Container = styled.div` - display: flex; - width: 100%; - flex-direction: column; - padding-bottom: 20px; - background-color: white; + display: flex; + width: 100%; + flex-direction: column; + padding-bottom: 20px; + background-color: white; `; const ProfileImgContainer = styled.div` - width: 200px; - display: flex; - justify-content: center; - align-items: center; + width: 200px; + display: flex; + justify-content: center; + align-items: center; `; const ProfileDetailContainer = styled.div` - width: 200px; - display: flex; - width: 100%; - flex-direction: row; - padding: 20px 0; - z-index: 99; - background-color: white; + width: 200px; + display: flex; + width: 100%; + flex-direction: row; + padding: 20px 0; + z-index: 99; + background-color: white; `; const MannerDegreeContainer = styled.div` - display: flex; - flex-direction: row; - gap: 16px; - align-items: center; + display: flex; + flex-direction: row; + gap: 16px; + align-items: center; `; const UserInfo = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - margin-bottom: 8px; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin-bottom: 8px; `; const Name = styled.div` - margin-bottom: 16px; + margin-bottom: 16px; `; const ProfileDetail = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - width: 60%; - padding: 20px; - gap: 8px; + display: flex; + flex-direction: column; + justify-content: center; + width: 60%; + padding: 20px; + gap: 8px; `; -const userId = 11; -// 로그인 기능 연결후 userid 받아올 예정 - const ProfileInfo = () => { - const { data } = useSuspenseQuery({ - queryKey: [API_GET_PROFILE_KEY], - queryFn: () => getProfile(), - }); + const { data } = useSuspenseQuery({ + queryKey: [API_GET_PROFILE_KEY], + queryFn: getProfile, + }); - if (!data) { - return; - } + const { gender, age, nickname, imgUrl, negativeReviewCount, positiveReviewCount } = data; - const { gender, age, socialType, nickname, imgUrl } = data; + const mannerDegree = useMemo(() => { + const basicDegree = 36.5; + return basicDegree + (negativeReviewCount * -0.5 + positiveReviewCount * 0.5); + }, [negativeReviewCount, positiveReviewCount]); - return ( - - - - {"profile-image"} - - - - - - - - - - - - - {30}°C - {/* 매너온도는 후기기능에 포함되어 보류 */} - - - - - ); + return ( + + + + {'profile-image'} + + + + + + + + + + + + + {mannerDegree}°C + + + + + ); }; export default ProfileInfo; diff --git a/src/components/profile/ProfileReviewList.tsx b/src/components/profile/ProfileReviewList.tsx deleted file mode 100644 index f04e86a..0000000 --- a/src/components/profile/ProfileReviewList.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import styled from '@emotion/styled'; -import { useRouter } from 'next/router'; -import { FC, useMemo } from 'react'; -import { useSearchParam } from 'react-use'; -import ProfileTabSortingButton from './ProfileTabSortingButton'; -import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { ProfileReviewListRequestType } from 'src/api/getProfileReviewList'; -import ProfileReviewCard from '../common/card/ReviewCard'; -import { GetReviewListResponse, ImageType } from 'types/review'; -import dayjs from 'dayjs'; -import React from 'react'; -import { useModalContext } from '@mantine/core/lib/components/Modal/Modal.context'; -import ReviewCard from '../common/card/ReviewCard'; - -interface ProfileReviewListProps {} -type ProfileReviewListType = '보낸리뷰' | '받은리뷰'; - -interface CategoryItemType { - id: ProfileReviewListRequestType; - label: ProfileReviewListType; -} - -const categoryTab: CategoryItemType[] = [ - { id: 'SENDER', label: '보낸리뷰' }, - { id: 'RECEIVER', label: '받은리뷰' }, -]; - -const Container = styled.div` - display: flex; - flex-direction: column; -`; - -const TabWrapper = styled.div` - display: flex; - gap: 10px; - padding: 10px; -`; - -const ProfileReviewListContainer = styled.div` - display: flex; - flex-direction: column; - gap: 30px; - padding: 16px; - overflow: auto; -`; - -function isProfileReviewListRole(value: unknown): value is ProfileReviewListRequestType { - return value === 'SENDER' || value === 'RECEIVER'; -} - -const ProfileReviewList: FC = () => { - const { query, replace } = useRouter(); - const reviewRole = useSearchParam('role'); - const selectedRole = useMemo(() => { - if (!reviewRole || !isProfileReviewListRole(reviewRole)) { - return; - } - return reviewRole; - }, [reviewRole]); - - // 랜덤 이미지를 picsum.photos에서 가져오는 함수 - function getRandomImageUrl(): string { - const width = 800; // 이미지 너비 - const height = 600; // 이미지 높이 - const randomNumber = Math.floor(Math.random() * 1000); // 랜덤 숫자 생성 (picsum.photos는 0부터 999까지의 이미지를 제공) - return `https://picsum.photos/${width}/${height}?random=${randomNumber}`; - } - - // 목데이터 생성 - function generateMockReviewList(numReviews: number): GetReviewListResponse[] { - const mockReviewList: GetReviewListResponse[] = []; - - for (let i = 1; i <= numReviews; i++) { - const reviewImg: ImageType[] = []; - const numImages = Math.floor(Math.random() * 5); // 랜덤으로 이미지 개수 생성 - - // 랜덤 이미지 추가 - for (let j = 0; j < numImages; j++) { - reviewImg.push({ - id: `image_${j}`, - imageUrl: getRandomImageUrl(), - }); - } - - mockReviewList.push({ - reviewId: i, - userProfileImg: `https://picsum.photos/100/100?random=${i}`, // 프로필 이미지는 각각 다른 이미지로 설정 - nickname: `user${i}`, - rating: Math.floor(Math.random() * 5) + 1, // 1에서 5까지의 랜덤한 평점 설정 - content: `Review content ${i}`, - createdAt: dayjs().subtract(i, 'day').format('YYYY-MM-DD'), - reviewImg: reviewImg, - }); - } - - return mockReviewList; - } - - // 목데이터 생성 - const numReviews = 10; // 생성할 리뷰 개수 - const mockReviewList = generateMockReviewList(numReviews); - - // const reviewData = useSuspenseQuery({ - // queryKey: [API_GET_REVIEW_LIST_KEY, { role: selectedRole }], - // queryFn: () => getProfileReviewList({ reviewType: selectedRole ?? 'RECEIVER' }), - // }); - - // console.log(reviewData); - - return ( - - - {categoryTab.map((tab) => { - const onClick = () => { - replace({ query: { ...query, role: tab.id } }); - }; - - return ( - - ); - })} - - - - {/* */} - - {mockReviewList.map((review) => ( - - ))} - {/* */} - {/* */} - - - ); -}; - -export default React.memo(ProfileReviewList); diff --git a/src/components/profile/ProfileTab.tsx b/src/components/profile/ProfileTab.tsx index da4b23d..a25ccb9 100644 --- a/src/components/profile/ProfileTab.tsx +++ b/src/components/profile/ProfileTab.tsx @@ -2,11 +2,11 @@ import styled from '@emotion/styled'; import { useRouter } from 'next/router'; import { useMemo } from 'react'; import { useSearchParam } from 'react-use'; -import PartySituation from './PartySituation'; +import PartySituation from './party/status/PartySituation'; import TabComponent from './TabComponent'; -import PartyRequest from './PartyRequest'; -import ReviewList from './ProfileReviewList'; -import ProfileReviewList from './ProfileReviewList'; +import PartyRequest from './party/request/PartyRequest'; +import ReviewList from './review/ProfileReview'; +import ProfileReviewList from './review/ProfileReview'; interface CategoryItemType { id: CategoryIdType; @@ -78,7 +78,7 @@ const ProfileTab = () => { replace({ query: { category: id, role: 'SENDER' } }); return; } - replace({ query: { category: id, role: 'HOST' } }); + replace({ query: { category: id, role: 'VOLUNTEER', status: 'RECRUIT' } }); }; return ( diff --git a/src/components/profile/ProfileTabPanel.tsx b/src/components/profile/ProfileTabPanel.tsx deleted file mode 100644 index d8cc737..0000000 --- a/src/components/profile/ProfileTabPanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Box from "@mui/material/Box"; -import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; -import ProfileError from "./ProfileError"; -import ProfileLoading from "./ProfileLoading"; - -interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; -} - -const ProfileTabPanel = (props: TabPanelProps) => { - const { children, value, index, ...other } = props; - - return ( - } - > - - - ); -}; - -export default ProfileTabPanel; diff --git a/src/components/profile/ToggleButton.tsx b/src/components/profile/ToggleButton.tsx deleted file mode 100644 index eb54c34..0000000 --- a/src/components/profile/ToggleButton.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import styled from "@emotion/styled"; -import { DefaultButton } from "@components/common/DefaultButton"; - -interface ToggleButtonProps { - partystate: string; - setPartystate: React.Dispatch>; -} - -const Container = styled.div` - display: flex; - background-color: white; - padding: 16px; - flex-direction: row; - gap: 10px; - transition: top 0.3s ease; - z-index: 9; -`; - -const ButtonList = [ - { - text: "모집중", - value: "host", - }, - { - text: "참가중", - value: "member", - }, -]; - -const ToggleButton = ({ partystate, setPartystate }: ToggleButtonProps) => { - return ( - - {ButtonList.map(({ text, value }) => ( - { - setPartystate(value); - }} - /> - ))} - - ); -}; - -export default ToggleButton; diff --git a/src/components/profile/party/PartyList.tsx b/src/components/profile/party/PartyList.tsx new file mode 100644 index 0000000..5657711 --- /dev/null +++ b/src/components/profile/party/PartyList.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { DefaultText } from '@components/common/DefaultText'; +import Image from 'next/image'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; +import Link from 'next/link'; +import dayjs from 'dayjs'; +import { GetPartyCurrentSituationResponse } from 'types/party'; +interface PartyListProps { + data: PartyDetailResponse | GetPartyCurrentSituationResponse; +} + +const Container = styled.div` + display: flex; + flex-direction: row; + gap: 16px; + padding: 16px; + border-bottom: 1px solid #ebebeb; + transition: all 0.1s; + &:hover { + background-color: #dddddd; + } + cursor: pointer; +`; + +const PartyDetail = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; +`; +const Title = styled.div` + display: flex; + flex-direction: row; +`; +const Address = styled.div` + display: flex; + flex-direction: row; + gap: 4px; +`; +const Time = styled.div` + display: flex; + flex-direction: row; + gap: 4px; +`; + +const PartyList = ({ data }: PartyListProps) => { + const { partyId, partyTitle, partyTime, address, thumbnail } = data; + + return ( + + + 프로필사진 + + + <DefaultText text={partyTitle} size={16} weight={500} /> + +
+ +
+ +
+
+ + ); +}; + +export default PartyList; diff --git a/src/components/profile/PartyRequest.tsx b/src/components/profile/party/request/PartyRequest.tsx similarity index 61% rename from src/components/profile/PartyRequest.tsx rename to src/components/profile/party/request/PartyRequest.tsx index 643eef2..f9ff1f9 100644 --- a/src/components/profile/PartyRequest.tsx +++ b/src/components/profile/party/request/PartyRequest.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; import styled from '@emotion/styled'; -import ButtonList from './ProfileTabSortingButton'; +import ButtonList from '../../ProfileTabSortingButton'; import { useQuery } from '@tanstack/react-query'; import getPartyJoin from 'src/api/getPartyJoin'; import { API_GET_PARTY_JOIN_KEY } from 'src/api/getPartyJoin'; @@ -8,10 +8,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import postPartyDecision from 'src/api/postPartyDecision'; import postParticipate from 'src/api/postParticipate'; import { useRouter } from 'next/router'; -import ProfileTabSortingButton from './ProfileTabSortingButton'; +import ProfileTabSortingButton from '../../ProfileTabSortingButton'; import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; import PartyRequestItemList from './PartyRequestItemList'; import { useSearchParam } from 'react-use'; +import { DefaultText } from '@components/common/DefaultText'; type PartyRequestType = '받은요청' | '보낸요청'; export type PartyRequestRole = 'HOST' | 'VOLUNTEER'; @@ -43,6 +44,11 @@ const PartyRequestContainer = styled.div` padding: 0 16px; overflow: auto; `; +const LoginRequiredTextWrapper = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; function isPartyRequestRole(value: unknown): value is PartyRequestRole { return value === 'HOST' || value === 'VOLUNTEER'; @@ -58,55 +64,6 @@ const PartyRequest = () => { return requestRole; }, [requestRole]); - // const queryClient = useQueryClient(); - - // const { data } = useQuery({ - // queryKey: [API_GET_PARTY_JOIN_KEY, { role }], - // queryFn: () => getPartyJoin({ role }), - // enabled: !!role, - // }); - - // const postDecisionMutate = useMutation({ - // mutationFn: postPartyDecision, - // onSuccess: () => { - // queryClient.invalidateQueries({ - // queryKey: [API_GET_PARTY_JOIN_KEY, { role }], - // }); - // }, - // }); - - // const postParticipateMutate = useMutation({ - // mutationFn: postParticipate, - // onSuccess: () => { - // queryClient.invalidateQueries({ - // queryKey: [API_GET_PARTY_JOIN_KEY, { role }], - // }); - // }, - // }); - - // const joinDecision = (id: number, nickname: string, status: boolean) => { - // if (role === 'HOST') { - // return postDecisionMutate.mutate({ - // nickname: nickname, - // partyId: Number(id), - // status: `${status ? 'ACCEPT' : 'REFUSE'}`, - // }); - // } - // postParticipateMutate.mutate({ - // partyId: Number(id), - // status: `${status ? 'APPLY' : 'CANCEL'}`, - // }); - // }; - - // const setButtonState = (state: string) => { - // router.replace({ - // query: { - // ...router.query, - // role: state, - // }, - // }); - // }; - return ( @@ -127,7 +84,22 @@ const PartyRequest = () => { - + { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} + > diff --git a/src/components/profile/PartyRequestCard.tsx b/src/components/profile/party/request/PartyRequestCard.tsx similarity index 100% rename from src/components/profile/PartyRequestCard.tsx rename to src/components/profile/party/request/PartyRequestCard.tsx diff --git a/src/components/profile/party/request/PartyRequestItemList.tsx b/src/components/profile/party/request/PartyRequestItemList.tsx new file mode 100644 index 0000000..9cc0aab --- /dev/null +++ b/src/components/profile/party/request/PartyRequestItemList.tsx @@ -0,0 +1,66 @@ +import { DefaultText } from '@components/common/DefaultText'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import styled from '@emotion/styled'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { FC } from 'react'; +import getPartyJoin, { API_GET_PARTY_JOIN_KEY } from 'src/api/getPartyJoin'; +import { PartyRequestRole } from './PartyRequest'; +import PartyRequestCard from './PartyRequestCard'; + +interface PartyRequestItemListProps { + role: PartyRequestRole; +} + +const Container = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + height: 200px; +`; + +const PartyRequestItemList: FC = ({ role }) => { + const partyRequestList = useSuspenseInfiniteQuery({ + queryKey: [API_GET_PARTY_JOIN_KEY, { role }], + queryFn: ({ pageParam = 0 }) => + getPartyJoin({ + page: pageParam, + role, + size: 5, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, + }); + const onObserve = () => { + if (partyRequestList.hasNextPage) partyRequestList.fetchNextPage(); + }; + + if (!partyRequestList.data.pages[0].partyList.length) { + return ( + + + + ); + } + + return ( + + {partyRequestList.data.pages.map((request) => + request.partyList.map((individualRequest) => ( + + )), + )} + + ); +}; + +export default PartyRequestItemList; diff --git a/src/components/profile/party/status/PartySituation.tsx b/src/components/profile/party/status/PartySituation.tsx new file mode 100644 index 0000000..c108d48 --- /dev/null +++ b/src/components/profile/party/status/PartySituation.tsx @@ -0,0 +1,157 @@ +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; +import styled from '@emotion/styled'; +import { useEffect, useMemo, useState } from 'react'; +import PartySituationItemList from './PartySituationItemList'; +import ProfileTabSortingButton from '../../ProfileTabSortingButton'; +import { useRouter } from 'next/router'; +import { useSearchParam } from 'react-use'; +import { PartyCurrentSituationRequestStatus } from 'types/party'; +import { DefaultText } from '@components/common/DefaultText'; + +type PartySituationType = '참여' | '개설'; +export type PartyStatusRole = 'HOST' | 'VOLUNTEER'; +type PartyCurrentStatus = '모집중' | '모집마감' | '종료'; + +interface CategoryItemType { + id: PartyStatusRole; + label: PartySituationType; +} + +interface StatusItemType { + id: PartyCurrentSituationRequestStatus; + label: PartyCurrentStatus; +} + +const Container = styled.div` + display: flex; + flex-direction: column; +`; +const PartyListContainer = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding: 0 16px; + overflow: auto; +`; +const TabWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; +`; + +const TabSection = styled.section` + display: flex; + gap: 10px; +`; + +const LoginRequiredTextWrapper = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; + +const categoryTab: CategoryItemType[] = [ + { id: 'VOLUNTEER', label: '참여' }, + { id: 'HOST', label: '개설' }, +]; + +const statusTab: StatusItemType[] = [ + { id: 'RECRUIT', label: '모집중' }, + { + id: 'RECRUIT_FINISH', + label: '모집마감', + }, + { id: 'PARTY_FINISH', label: '종료' }, +]; + +function isPartyRole(value: unknown): value is PartyStatusRole { + return value === 'HOST' || value === 'VOLUNTEER'; +} +function isPartySituation(value: unknown): value is PartyCurrentSituationRequestStatus { + return value === 'RECRUIT' || value === 'RECRUIT_FINISH' || value === 'PARTY_FINISH'; +} + +const PartySituation = () => { + const { replace, query } = useRouter(); + const role = useSearchParam('role'); + const status = useSearchParam('status'); + const selectedRole = useMemo(() => { + if (!role || !isPartyRole(role)) { + return; + } + return role; + }, [role]); + + const selectedStatus = useMemo(() => { + if (!status || !isPartySituation(status)) { + return; + } + return status; + }, [status]); + + return ( + + + + {categoryTab.map((tab) => { + const onClick = () => { + replace({ query: { ...query, role: tab.id } }); + }; + + return ( + + ); + })} + + + {statusTab.map((tab) => { + const onClick = () => { + replace({ query: { ...query, status: tab.id } }); + }; + + return ( + + ); + })} + + + + + { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} + > + + + + + ); +}; + +export default PartySituation; diff --git a/src/components/profile/party/status/PartySituationItemList.tsx b/src/components/profile/party/status/PartySituationItemList.tsx new file mode 100644 index 0000000..8dcdbfe --- /dev/null +++ b/src/components/profile/party/status/PartySituationItemList.tsx @@ -0,0 +1,67 @@ +import { DefaultText } from '@components/common/DefaultText'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import styled from '@emotion/styled'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { FC } from 'react'; +import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyCurrentSituation'; +import { PartyCurrentSituationRequestStatus } from 'types/party'; +import PartyList from '../PartyList'; +import { PartyStatusRole } from './PartySituation'; + +interface PartySituationItemListProps { + selectedRole: PartyStatusRole; + selectedStatus: PartyCurrentSituationRequestStatus; +} + +const Container = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + height: 200px; +`; + +const PartySituationItemList: FC = ({ + selectedRole, + selectedStatus, +}) => { + const partyStatusList = useSuspenseInfiniteQuery({ + queryKey: [API_GET_PARTY_STATUS_KEY, { role: selectedRole }], + queryFn: ({ pageParam = 0 }) => + getPartyStatus({ + page: pageParam, + role: selectedRole, + status: selectedStatus, + size: 5, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, + }); + const onObserve = () => { + if (partyStatusList.hasNextPage) partyStatusList.fetchNextPage(); + }; + if (!partyStatusList.data.pages[0].partyList.length) { + return ( + + + + ); + } + + return ( + + {partyStatusList.data.pages.map((status) => + status.partyList.map((individualStatus) => ( + + )), + )} + + ); +}; + +export default PartySituationItemList; diff --git a/src/components/profile/review/ProfileReview.tsx b/src/components/profile/review/ProfileReview.tsx new file mode 100644 index 0000000..7cad5c5 --- /dev/null +++ b/src/components/profile/review/ProfileReview.tsx @@ -0,0 +1,106 @@ +import { DefaultText } from '@components/common/DefaultText'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; +import styled from '@emotion/styled'; +import { useRouter } from 'next/router'; +import React, { FC, useMemo } from 'react'; +import { useSearchParam } from 'react-use'; +import { ReviewListRequestType } from 'src/api/getReviewList'; +import ProfileTabSortingButton from '../ProfileTabSortingButton'; +import ProfileReviewItemList from './ProfileReviewItemList'; + +interface ProfileReviewProps {} +type ProfileReviewType = '보낸리뷰' | '받은리뷰'; + +interface CategoryItemType { + id: ReviewListRequestType; + label: ProfileReviewType; +} + +const categoryTab: CategoryItemType[] = [ + { id: 'SENDER', label: '보낸리뷰' }, + { id: 'RECEIVER', label: '받은리뷰' }, +]; + +const LoginRequiredTextWrapper = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const TabWrapper = styled.div` + display: flex; + gap: 10px; + padding: 10px; +`; + +const ProfileReviewContainer = styled.div` + display: flex; + flex-direction: column; + gap: 30px; + padding: 16px; + overflow: auto; +`; + +function isProfileReviewRole(value: unknown): value is ReviewListRequestType { + return value === 'SENDER' || value === 'RECEIVER'; +} + +const ProfileReview: FC = () => { + const { query, replace } = useRouter(); + const reviewRole = useSearchParam('role'); + const selectedRole = useMemo(() => { + if (!reviewRole || !isProfileReviewRole(reviewRole)) { + return; + } + return reviewRole; + }, [reviewRole]); + + return ( + + + {categoryTab.map((tab) => { + const onClick = () => { + replace({ query: { ...query, role: tab.id } }); + }; + + return ( + + ); + })} + + + + { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} + > + + + + + ); +}; + +export default React.memo(ProfileReview); diff --git a/src/components/profile/review/ProfileReviewItemList.tsx b/src/components/profile/review/ProfileReviewItemList.tsx new file mode 100644 index 0000000..714f74d --- /dev/null +++ b/src/components/profile/review/ProfileReviewItemList.tsx @@ -0,0 +1,65 @@ +import ReviewCard from '@components/common/card/ReviewCard'; +import { DefaultText } from '@components/common/DefaultText'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import styled from '@emotion/styled'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { FC } from 'react'; +import getReviewList, { + API_GET_REVIEW_LIST_KEY, + ReviewListRequestType, +} from 'src/api/getReviewList'; + +interface ProfileReviewItemListProps { + role: ReviewListRequestType; +} + +const Container = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + height: 200px; +`; + +const ProfileReviewItemList: FC = ({ role }) => { + const reviewList = useSuspenseInfiniteQuery({ + queryKey: [API_GET_REVIEW_LIST_KEY, { role }], + queryFn: ({ pageParam = 0 }) => + getReviewList({ + page: pageParam, + reviewType: role, + size: 5, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, + }); + + const onObserve = () => { + if (reviewList.hasNextPage) reviewList.fetchNextPage(); + }; + + if (!reviewList.data.pages[0].reviewGetResList.length) { + return ( + + + + ); + } + + return ( + + {reviewList.data.pages.map((review) => + review.reviewGetResList.map((individualReview) => ( + + )), + )} + + ); +}; + +export default ProfileReviewItemList; diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx index 0a323ec..40b92a0 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/search/SearchResult.tsx @@ -1,85 +1,88 @@ -import { DefaultText } from "@components/common/DefaultText"; -import NoResult from "@components/common/NoResult"; -import { ObserverTrigger } from "@components/hoc/ObserverTrigger"; -import { PartyCard } from "@components/home/PartyCard"; -import styled from "@emotion/styled"; -import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; -import { useRouter } from "next/router"; -import { FC } from "react"; -import getSearchResult, { - API_GET_SEARCH_RESULT, -} from "src/api/getSearchResult"; -import { Color } from "styles/Color"; +import { DefaultText } from '@components/common/DefaultText'; +import NoResult from '@components/common/NoResult'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import { PartyCard } from '@components/home/PartyCard'; +import styled from '@emotion/styled'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { FC } from 'react'; +import getSearchResult, { API_GET_SEARCH_RESULT } from 'src/api/getSearchResult'; +import { Color } from 'styles/Color'; interface SearchResultProps { - keyword: string; + keyword: string; } const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; - background-color: ${Color.VeryLightGrey}; - overflow-y: scroll; - overflow-x: hidden; - align-items: center; - padding: 0 15px 60px 15px; + display: flex; + flex-direction: column; + width: 100%; + background-color: ${Color.VeryLightGrey}; + overflow-y: scroll; + overflow-x: hidden; + align-items: center; + padding: 0 15px 60px 15px; `; export const SearchResult: FC = ({ keyword }) => { - const router = useRouter(); + const router = useRouter(); - const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ - queryKey: [API_GET_SEARCH_RESULT, { keyword }], - queryFn: ({ pageParam = 0 }) => - getSearchResult({ - keyword, - lastPartyId: pageParam, - }), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.lastPartyId, - staleTime: 0, - }); + const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ + queryKey: [API_GET_SEARCH_RESULT, { keyword }], + queryFn: ({ pageParam = 0 }) => + getSearchResult({ + keyword, + lastPartyId: pageParam, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, + staleTime: 0, + }); - const onClickPartyCard = (id: number) => { - router.push(`/party/${id}`); - }; - const onObserve = () => { - if (hasNextPage) fetchNextPage(); - }; + const onClickPartyCard = (id: number) => { + router.push(`/party/${id}`); + }; + const onObserve = () => { + if (hasNextPage) fetchNextPage(); + }; - if (!data || !data.pages) { - return; - } + if (!data || !data.pages) { + return; + } - const partyData = data.pages; + const partyData = data.pages; - return ( - - {partyData[0]?.partyList.length > 0 ? ( - - {partyData.map((item) => - item?.partyList.map((party) => ( - - )) - )} - - ) : ( - - - - )} - - ); + return ( + + {partyData[0]?.partyList.length > 0 ? ( + + {partyData.map((item) => + item?.partyList.map((party) => ( + + )), + )} + + ) : ( + + + + )} + + ); }; diff --git a/src/components/search/header/index.tsx b/src/components/search/header/index.tsx index 8b0d5f0..030cfe0 100644 --- a/src/components/search/header/index.tsx +++ b/src/components/search/header/index.tsx @@ -1,6 +1,10 @@ import styled from '@emotion/styled'; import { useSearchKeyword } from '@hooks/useSearchKeyword'; +import { useRouter } from 'next/router'; import { FC, RefObject, forwardRef } from 'react'; +import { useSearchParam } from 'react-use'; +import { useRecoilValue } from 'recoil'; +import { recentKeywordStates } from 'src/recoil-states/recentKeywordStates'; interface CenterProps {} @@ -16,9 +20,16 @@ const SearchInputContainer = styled.div` const Center: FC = () => { const { searchKeyword, inputRef } = useSearchKeyword(); + const { query } = useRouter(); + const { keyword } = query; return ( - + ); }; diff --git a/src/lib/axios/defaultRequest.ts b/src/lib/axios/defaultRequest.ts index 50bd1fa..d207971 100644 --- a/src/lib/axios/defaultRequest.ts +++ b/src/lib/axios/defaultRequest.ts @@ -1,50 +1,50 @@ -import axios from "axios"; -import { getCookie } from "cookies-next"; +import axios from 'axios'; +import { getCookie } from 'cookies-next'; const defaultRequest = axios.create({ - baseURL: process.env.MATITTING_HOST_URL, - headers: { - "Content-Type": "application/json", - }, - withCredentials: true, + baseURL: process.env.MATITTING_HOST_URL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, }); defaultRequest.interceptors.response.use( - async function (response) { - return response; - }, - async function (error) { - if (error.response && error.response.status === 401) { - const refreshToken = getCookie("refreshToken"); + async function (response) { + return response; + }, + async function (error) { + if (error.response && error.response.status === 401) { + const refreshToken = getCookie('refreshToken'); - if (!refreshToken) { - await alert("로그인이 필요합니다. 로그인 해 주세요."); - window.location.href = "/signin"; - return Promise.reject(error); - } + if (!refreshToken) { + // await alert('로그인이 필요합니다. 로그인 해 주세요.'); + // window.location.href = '/signin'; + return Promise.reject(error); + } - try { - const response = await defaultRequest.get("/oauth2/renew-token", { - headers: { - "Authorization-Refresh": refreshToken, - }, - }); + try { + const response = await defaultRequest.get('/oauth2/renew-token', { + headers: { + 'Authorization-Refresh': refreshToken, + }, + }); - // 토큰 갱신 성공 시 새로운 토큰으로 요청 재시도 - defaultRequest.defaults.headers["Authorization"] = - response.headers["authorization"]; - return defaultRequest.request(error.config); - } catch (refreshError) { - // 토큰 갱신에 실패하면 에러 반환 - await alert("토큰 갱신에 실패했습니다. 로그인 페이지로 이동합니다."); - window.location.href = "/signin"; // Redirect to sign-in page - return Promise.reject(refreshError); - } - } + // 토큰 갱신 성공 시 새로운 토큰으로 요청 재시도 + defaultRequest.defaults.headers['Authorization'] = + response.headers['authorization']; + return defaultRequest.request(error.config); + } catch (refreshError) { + // 토큰 갱신에 실패하면 에러 반환 + // await alert('토큰 갱신에 실패했습니다. 로그인 페이지로 이동합니다.'); + // window.location.href = '/signin'; // Redirect to sign-in page + return Promise.reject(refreshError); + } + } - // 401 상태 코드가 아닌 경우에는 그대로 오류 반환 - return Promise.reject(error); - } + // 401 상태 코드가 아닌 경우에는 그대로 오류 반환 + return Promise.reject(error); + }, ); export default defaultRequest; diff --git a/types/common/InfinitePaginationDataType.ts b/types/common/InfinitePaginationDataType.ts index 5ae5dc7..0fc8b01 100644 --- a/types/common/InfinitePaginationDataType.ts +++ b/types/common/InfinitePaginationDataType.ts @@ -1,8 +1,8 @@ type InfinitePaginationDataType = { - [key in K]: T[]; + [key in K]: T[]; } & { - pageInfo: { - lastPartyId: number; - hasNext: boolean; - }; + pageInfo: { + page: number; + hasNext: boolean; + }; }; diff --git a/types/party.ts b/types/party.ts deleted file mode 100644 index 3b4b522..0000000 --- a/types/party.ts +++ /dev/null @@ -1,13 +0,0 @@ -interface PartyData { - categoryId: string; - thumbnailUrl: string; - partyTitle: string; - region: string; - partyTime: string; - genderLimit: string; - agePreference: string; - partyMessage: string; - totalRecruitment: string; -} - -export type { PartyData }; diff --git a/types/party/index.ts b/types/party/index.ts new file mode 100644 index 0000000..51c8e14 --- /dev/null +++ b/types/party/index.ts @@ -0,0 +1,28 @@ +import { UserGender } from 'types/profile/user/UserProfileResponse'; +import { PartyAge } from './join/PartyJoinResponse'; + +export type PartyCurrentSituationRequestStatus = 'RECRUIT' | 'RECRUIT_FINISH' | 'PARTY_FINISH'; +export type PartyFoodCategory = 'KOREAN' | 'WESTERN' | 'JAPANESE' | 'CHINESE' | 'ETC'; + +export interface GetPartyCurrentSituationResponse { + userId: number; + partyId: number; + partyTitle: string; + partyContent: string; + address: string; + longitude: number; + latitude: number; + partyPlaceName: string; + status: PartyCurrentSituationRequestStatus; + gender: UserGender; + age: PartyAge; + deadline: string; + partyTime: string; + totalParticipate: 4; + participate: 2; + menu: string; + category: PartyFoodCategory; + thumbnail: string; + hit: number; + reviewExist: boolean; +} diff --git a/types/party/join/PartyJoinResponse.ts b/types/party/join/PartyJoinResponse.ts index 942101c..d0da8af 100644 --- a/types/party/join/PartyJoinResponse.ts +++ b/types/party/join/PartyJoinResponse.ts @@ -1,10 +1,17 @@ +import { UserGender } from 'types/profile/user/UserProfileResponse'; + +export type PartyAge = 'ALL' | 'TWENTY' | 'THIRTY' | 'FORTY'; + export interface PartyJoinResponse { - partyId: number; - partyTitle: string; - nickname: string; - imgUrl: string; - partyGender: string; - partyAge: string; - userGender: string; - userAge: number; + partyId: number; + partyTitle: string; + nickname: string; + imgUrl: string; + partyGender: string; + partyAge: PartyAge; + userGender: UserGender; + userAge: number; + createAt: string; + oneLineIntroduce: string; + typeMatch: boolean; } diff --git a/types/profile/user/UserProfileResponse.ts b/types/profile/user/UserProfileResponse.ts index b5f7d35..8a94758 100644 --- a/types/profile/user/UserProfileResponse.ts +++ b/types/profile/user/UserProfileResponse.ts @@ -1,13 +1,19 @@ +export type UserGender = 'ALL' | 'MALE' | 'FEMALE' | 'UNKNOWN'; + +export type OauthProvider = 'KAKAO' | 'NAVER'; +export type UserRole = 'GUEST' | 'USER' | 'VOLUNTEER' | 'HOST'; + export interface UserProfileResponse { - createDate: string; - modifiedDate: string; - id: number; - socialId: string; - socialType: string; - email: string; - nickname: string; - age: number; - imgUrl: string; - gender: string; - role: string; + userId: number; + socialId: string; + oauthProvider: string; + email: string; + nickname: string; + age: number; + imgUrl: string; + gender: UserGender; + role: UserRole; + rating: number; + positiveReviewCount: number; + negativeReviewCount: number; } diff --git a/types/review/index.ts b/types/review/index.ts index 5a98cb4..5e6f9f9 100644 --- a/types/review/index.ts +++ b/types/review/index.ts @@ -9,6 +9,6 @@ export interface GetReviewListResponse { nickname: string; rating: number; content: string; - reviewImg: ImageType[]; + reviewImg: string[]; createdAt: string | Date; }