From b71075a3219044c67ee0f04e3bcfbcc90062d305 Mon Sep 17 00:00:00 2001 From: minchodang Date: Wed, 24 Apr 2024 16:21:19 +0900 Subject: [PATCH 01/14] =?UTF-8?q?Fix:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=ED=8C=8C=ED=8B=B0=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20api=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A7=A4=EB=84=88=20=EC=98=A8?= =?UTF-8?q?=EB=8F=84=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/getPartyJoin.ts | 26 ++-- src/components/profile/ProfileInfo.tsx | 170 +++++++++++----------- types/party/join/PartyJoinResponse.ts | 23 ++- types/profile/user/UserProfileResponse.ts | 28 ++-- 4 files changed, 131 insertions(+), 116 deletions(-) diff --git a/src/api/getPartyJoin.ts b/src/api/getPartyJoin.ts index 4ebce7b..24bab38 100644 --- a/src/api/getPartyJoin.ts +++ b/src/api/getPartyJoin.ts @@ -1,20 +1,22 @@ -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'; +export type GetPartyJoinRequestRole = 'HOST' | 'VOLUNTEER'; interface getPartyJoinParameter { - role: string; + page: number; + size: number; + sort: string; + 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?role={{role}}'; -const getPartyJoin = async ({ - role, -}: getPartyJoinParameter): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_PARTY_JOIN_KEY, { role: role }) - ); - return data; +const getPartyJoin = async ({ role }: getPartyJoinParameter): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_PARTY_JOIN_KEY, { role: role }), + ); + return data; }; export default getPartyJoin; diff --git a/src/components/profile/ProfileInfo.tsx b/src/components/profile/ProfileInfo.tsx index 29a170d..d324c2b 100644 --- a/src/components/profile/ProfileInfo.tsx +++ b/src/components/profile/ProfileInfo.tsx @@ -1,117 +1,117 @@ -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/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; } From 8cc096d71a718f08cf13d03e2b6fb0fe7a9bbdfd Mon Sep 17 00:00:00 2001 From: minchodang Date: Wed, 24 Apr 2024 17:45:23 +0900 Subject: [PATCH 02/14] =?UTF-8?q?Fix:=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EB=A6=AC=EC=8A=A4=ED=8F=B0=EC=8A=A4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/getPartyJoin.ts | 14 +- src/components/home/HomeList.tsx | 2 +- .../profile/PartyRequestItemList.tsx | 38 ++++- src/components/search/SearchResult.tsx | 140 +++++++++--------- types/common/InfinitePaginationDataType.ts | 10 +- 5 files changed, 114 insertions(+), 90 deletions(-) diff --git a/src/api/getPartyJoin.ts b/src/api/getPartyJoin.ts index 24bab38..7269c90 100644 --- a/src/api/getPartyJoin.ts +++ b/src/api/getPartyJoin.ts @@ -10,12 +10,16 @@ interface getPartyJoinParameter { 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 }), - ); +const getPartyJoin = async (params: getPartyJoinParameter) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'partyList', PartyJoinResponse> + >(API_GET_PARTY_JOIN_KEY, { + params: { + ...params, + }, + }); return data; }; diff --git a/src/components/home/HomeList.tsx b/src/components/home/HomeList.tsx index e82afd7..c17a437 100644 --- a/src/components/home/HomeList.tsx +++ b/src/components/home/HomeList.tsx @@ -41,7 +41,7 @@ export const HomeList: FC = () => { }) : Promise.resolve(null), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.lastPartyId, + getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, }); const onClickPartyCard = (id: number) => { diff --git a/src/components/profile/PartyRequestItemList.tsx b/src/components/profile/PartyRequestItemList.tsx index 27ddef4..f2f92e5 100644 --- a/src/components/profile/PartyRequestItemList.tsx +++ b/src/components/profile/PartyRequestItemList.tsx @@ -1,4 +1,4 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; +import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; import { FC } from 'react'; import getPartyJoin, { API_GET_PARTY_JOIN_KEY } from 'src/api/getPartyJoin'; import { PartyRequestRole } from './PartyRequest'; @@ -6,6 +6,7 @@ import PartyRequestList from './PartyRequestCard'; import { DefaultText } from '@components/common/DefaultText'; import styled from '@emotion/styled'; import PartyRequestCard from './PartyRequestCard'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; interface PartyRequestItemListProps { role: PartyRequestRole; @@ -20,12 +21,23 @@ const Container = styled.div` `; const PartyRequestItemList: FC = ({ role }) => { - const requestList = useSuspenseQuery({ - queryKey: [API_GET_PARTY_JOIN_KEY, { role }], - queryFn: () => getPartyJoin({ role }), + const partyRequestList = useSuspenseInfiniteQuery({ + queryKey: [API_GET_PARTY_JOIN_KEY, , { role }], + queryFn: ({ pageParam = 0 }) => + getPartyJoin({ + page: pageParam, + role, + sort: '', + size: 5, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, }); + const onObserve = () => { + if (partyRequestList.hasNextPage) partyRequestList.fetchNextPage(); + }; - if (!requestList.data.length) { + if (!partyRequestList.data.pages[0].partyList.length) { return ( @@ -33,9 +45,19 @@ const PartyRequestItemList: FC = ({ role }) => { ); } - return requestList.data.map((request) => ( - - )); + return ( + + {partyRequestList.data.pages.map((request) => + request.partyList.map((individualRequest) => ( + + )), + )} + + ); }; export default PartyRequestItemList; diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx index 0a323ec..2c24db6 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/search/SearchResult.tsx @@ -1,85 +1,83 @@ -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) => lastPage?.pageInfo?.page, + 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/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; + }; }; From 12cf63cf7782847770715c20aeb226fd49e654b9 Mon Sep 17 00:00:00 2001 From: minchodang Date: Thu, 25 Apr 2024 17:04:20 +0900 Subject: [PATCH 03/14] =?UTF-8?q?Fix:=20=ED=8C=8C=ED=8B=B0=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20api=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/getPartyCurrentSituation.ts | 25 ++++++ src/api/getPartyJoin.ts | 10 +-- src/api/getPartyMainPage.ts | 33 ++++---- src/api/getPartyStatus.ts | 20 ----- src/components/profile/PartySituation.tsx | 78 +++++++++++++++---- .../profile/PartySituationItemList.tsx | 2 +- types/party.ts | 13 ---- types/party/index.ts | 28 +++++++ 8 files changed, 136 insertions(+), 73 deletions(-) create mode 100644 src/api/getPartyCurrentSituation.ts delete mode 100644 src/api/getPartyStatus.ts delete mode 100644 types/party.ts create mode 100644 types/party/index.ts diff --git a/src/api/getPartyCurrentSituation.ts b/src/api/getPartyCurrentSituation.ts new file mode 100644 index 0000000..17c1001 --- /dev/null +++ b/src/api/getPartyCurrentSituation.ts @@ -0,0 +1,25 @@ +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; + sort: string; + 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 7269c90..ffe8ad6 100644 --- a/src/api/getPartyJoin.ts +++ b/src/api/getPartyJoin.ts @@ -2,8 +2,8 @@ import variableAssignMent from '@utils/variableAssignment'; import defaultRequest from 'src/lib/axios/defaultRequest'; import { PartyJoinResponse } from 'types/party/join/PartyJoinResponse'; -export type GetPartyJoinRequestRole = 'HOST' | 'VOLUNTEER'; -interface getPartyJoinParameter { +type GetPartyJoinRequestRole = 'HOST' | 'VOLUNTEER'; +interface GetPartyJoinParameter { page: number; size: number; sort: string; @@ -12,13 +12,11 @@ interface getPartyJoinParameter { export const API_GET_PARTY_JOIN_KEY = '/api/party/party-join'; -const getPartyJoin = async (params: getPartyJoinParameter) => { +const getPartyJoin = async (params: GetPartyJoinParameter) => { const { data } = await defaultRequest.get< InfinitePaginationDataType<'partyList', PartyJoinResponse> >(API_GET_PARTY_JOIN_KEY, { - params: { - ...params, - }, + params, }); return data; }; diff --git a/src/api/getPartyMainPage.ts b/src/api/getPartyMainPage.ts index c735b61..8e28eea 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; + lastPartyId?: 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/components/profile/PartySituation.tsx b/src/components/profile/PartySituation.tsx index 91381e7..99284ca 100644 --- a/src/components/profile/PartySituation.tsx +++ b/src/components/profile/PartySituation.tsx @@ -5,15 +5,22 @@ import PartySituationItemList from './PartySituationItemList'; import ProfileTabSortingButton from './ProfileTabSortingButton'; import { useRouter } from 'next/router'; import { useSearchParam } from 'react-use'; +import { PartyCurrentSituationRequestStatus } from 'types/party'; type PartySituationType = '모집중' | '참가중'; export type PartySituationRole = 'HOST' | 'VOLUNTEER'; +type PartyCurrentStatus = '모집중' | '모집완료' | '파티종료'; interface CategoryItemType { id: PartySituationRole; label: PartySituationType; } +interface StatusItemType { + id: PartyCurrentSituationRequestStatus; + label: PartyCurrentStatus; +} + const Container = styled.div` display: flex; flex-direction: column; @@ -27,23 +34,41 @@ const PartyListContainer = styled.div` `; const TabWrapper = styled.div` display: flex; + flex-direction: column; gap: 10px; padding: 10px; `; +const TabSection = styled.section` + display: flex; + gap: 10px; +`; + const categoryTab: CategoryItemType[] = [ { id: 'HOST', label: '모집중' }, { id: 'VOLUNTEER', label: '참가중' }, ]; +const statusTab: StatusItemType[] = [ + { id: 'RECRUIT', label: '모집중' }, + { + id: 'RECRUIT_FINISH', + label: '모집완료', + }, + { id: 'PARTY_FINISH', label: '파티종료' }, +]; + function isPartySituationRole(value: unknown): value is PartySituationRole { return value === 'HOST' || value === 'VOLUNTEER'; } +function isPartySituation(value: unknown): value is PartyCurrentSituationRequestStatus { + return value === 'RECRUIT' || value === 'RECRUIT_FINISH' || value === 'PARTY_FINISH'; +} const PartySituation = () => { - // const [selectedRole, setSelectedRole] = useState('HOST'); const { replace, query } = useRouter(); const situationRole = useSearchParam('role'); + const situation = useSearchParam('situation'); const selectedRole = useMemo(() => { if (!situationRole || !isPartySituationRole(situationRole)) { return; @@ -51,23 +76,48 @@ const PartySituation = () => { return situationRole; }, [situationRole]); + const selectedSituation = useMemo(() => { + if (!situation || !isPartySituation(situation)) { + return; + } + return situation; + }, [situation]); + return ( - {categoryTab.map((tab) => { - const onClick = () => { - replace({ query: { ...query, role: tab.id } }); - }; + + {categoryTab.map((tab) => { + const onClick = () => { + replace({ query: { ...query, role: tab.id } }); + }; + + return ( + + ); + })} + + + {statusTab.map((tab) => { + const onClick = () => { + replace({ query: { ...query, status: tab.id } }); + }; - return ( - - ); - })} + return ( + + ); + })} + diff --git a/src/components/profile/PartySituationItemList.tsx b/src/components/profile/PartySituationItemList.tsx index e82dbcd..3ec460c 100644 --- a/src/components/profile/PartySituationItemList.tsx +++ b/src/components/profile/PartySituationItemList.tsx @@ -1,6 +1,6 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { FC } from 'react'; -import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyStatus'; +import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyCurrentSituation'; import { PartySituationRole } from './PartySituation'; import PartyList from './PartyList'; import { DefaultText } from '@components/common/DefaultText'; 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; +} From 870bf4d7a1270b28d587a7ad9e75d2ecb4cd2931 Mon Sep 17 00:00:00 2001 From: minchodang Date: Sun, 28 Apr 2024 12:14:35 +0900 Subject: [PATCH 04/14] =?UTF-8?q?Fix:=20=ED=8C=8C=ED=8B=B0=20=ED=98=84?= =?UTF-8?q?=ED=99=A9,=20=EC=B4=88=EB=8C=80=20=EC=9A=94=EC=B2=AD=20api=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/profile/index.tsx | 31 ++++++- src/api/getPartyCurrentSituation.ts | 1 - src/api/getPartyJoin.ts | 1 - .../hoc/QuerySuspenseErrorBoundary.tsx | 75 +++++++++-------- src/components/profile/PartyList.tsx | 82 ------------------ .../profile/PartySituationItemList.tsx | 36 -------- src/components/profile/ProfileInfo.tsx | 3 - src/components/profile/ProfileTab.tsx | 10 +-- src/components/profile/ProfileTabPanel.tsx | 33 -------- src/components/profile/ToggleButton.tsx | 48 ----------- src/components/profile/party/PartyList.tsx | 83 +++++++++++++++++++ .../{ => party/request}/PartyRequest.tsx | 76 ++++++----------- .../{ => party/request}/PartyRequestCard.tsx | 0 .../request}/PartyRequestItemList.tsx | 3 +- .../{ => party/status}/PartySituation.tsx | 71 +++++++++++----- .../party/status/PartySituationItemList.tsx | 67 +++++++++++++++ .../{ => review}/ProfileReviewList.tsx | 6 +- src/lib/axios/defaultRequest.ts | 76 ++++++++--------- 18 files changed, 338 insertions(+), 364 deletions(-) delete mode 100644 src/components/profile/PartyList.tsx delete mode 100644 src/components/profile/PartySituationItemList.tsx delete mode 100644 src/components/profile/ProfileTabPanel.tsx delete mode 100644 src/components/profile/ToggleButton.tsx create mode 100644 src/components/profile/party/PartyList.tsx rename src/components/profile/{ => party/request}/PartyRequest.tsx (61%) rename src/components/profile/{ => party/request}/PartyRequestCard.tsx (100%) rename src/components/profile/{ => party/request}/PartyRequestItemList.tsx (95%) rename src/components/profile/{ => party/status}/PartySituation.tsx (59%) create mode 100644 src/components/profile/party/status/PartySituationItemList.tsx rename src/components/profile/{ => review}/ProfileReviewList.tsx (96%) diff --git a/pages/profile/index.tsx b/pages/profile/index.tsx index 94439d0..8baaaa1 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,7 +30,6 @@ const Container = styled.div` const ProfileInfoContainer = styled.div` display: flex; flex-direction: column; - height: 200px; width: 100%; `; @@ -40,6 +41,15 @@ const RightAreaContainer = styled.div` 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 +61,10 @@ const RightArea = () => { const Profile = () => { const scrollRef = useRef(null); const { y } = useScroll(scrollRef); + const { push } = useRouter(); + const onClickLoginButton = () => { + push('/signin'); + }; return ( @@ -58,7 +72,18 @@ const Profile = () => { { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} suspenseFallback={} > @@ -79,7 +104,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, - destination: '/profile?category=situation&role=HOST', + destination: '/profile?category=situation&role=HOST&status=RECRUIT', }, }; } diff --git a/src/api/getPartyCurrentSituation.ts b/src/api/getPartyCurrentSituation.ts index 17c1001..b3cfec4 100644 --- a/src/api/getPartyCurrentSituation.ts +++ b/src/api/getPartyCurrentSituation.ts @@ -6,7 +6,6 @@ export type GetPartyCurrentSituationRequestStatus = 'RECRUIT' | 'RECRUIT_FINISH' interface GetPartyCurrentSituationParameter { page: number; size: number; - sort: string; role: GetPartyCurrentSituationRequestRole; status: GetPartyCurrentSituationRequestStatus; } diff --git a/src/api/getPartyJoin.ts b/src/api/getPartyJoin.ts index ffe8ad6..4364b7f 100644 --- a/src/api/getPartyJoin.ts +++ b/src/api/getPartyJoin.ts @@ -6,7 +6,6 @@ type GetPartyJoinRequestRole = 'HOST' | 'VOLUNTEER'; interface GetPartyJoinParameter { page: number; size: number; - sort: string; role: GetPartyJoinRequestRole; } 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/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/PartySituationItemList.tsx b/src/components/profile/PartySituationItemList.tsx deleted file mode 100644 index 3ec460c..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/getPartyCurrentSituation'; -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 d324c2b..2c85610 100644 --- a/src/components/profile/ProfileInfo.tsx +++ b/src/components/profile/ProfileInfo.tsx @@ -64,9 +64,6 @@ const ProfileDetail = styled.div` gap: 8px; `; -const userId = 11; -// 로그인 기능 연결후 userid 받아올 예정 - const ProfileInfo = () => { const { data } = useSuspenseQuery({ queryKey: [API_GET_PROFILE_KEY], diff --git a/src/components/profile/ProfileTab.tsx b/src/components/profile/ProfileTab.tsx index da4b23d..a209254 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/ProfileReviewList'; +import ProfileReviewList from './review/ProfileReviewList'; 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/PartyRequestItemList.tsx b/src/components/profile/party/request/PartyRequestItemList.tsx similarity index 95% rename from src/components/profile/PartyRequestItemList.tsx rename to src/components/profile/party/request/PartyRequestItemList.tsx index f2f92e5..281d3da 100644 --- a/src/components/profile/PartyRequestItemList.tsx +++ b/src/components/profile/party/request/PartyRequestItemList.tsx @@ -27,7 +27,6 @@ const PartyRequestItemList: FC = ({ role }) => { getPartyJoin({ page: pageParam, role, - sort: '', size: 5, }), initialPageParam: 0, @@ -40,7 +39,7 @@ const PartyRequestItemList: FC = ({ role }) => { if (!partyRequestList.data.pages[0].partyList.length) { return ( - + ); } diff --git a/src/components/profile/PartySituation.tsx b/src/components/profile/party/status/PartySituation.tsx similarity index 59% rename from src/components/profile/PartySituation.tsx rename to src/components/profile/party/status/PartySituation.tsx index 99284ca..c108d48 100644 --- a/src/components/profile/PartySituation.tsx +++ b/src/components/profile/party/status/PartySituation.tsx @@ -1,18 +1,19 @@ import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; import styled from '@emotion/styled'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import PartySituationItemList from './PartySituationItemList'; -import ProfileTabSortingButton from './ProfileTabSortingButton'; +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 PartySituationRole = 'HOST' | 'VOLUNTEER'; -type PartyCurrentStatus = '모집중' | '모집완료' | '파티종료'; +type PartySituationType = '참여' | '개설'; +export type PartyStatusRole = 'HOST' | 'VOLUNTEER'; +type PartyCurrentStatus = '모집중' | '모집마감' | '종료'; interface CategoryItemType { - id: PartySituationRole; + id: PartyStatusRole; label: PartySituationType; } @@ -44,21 +45,27 @@ const TabSection = styled.section` gap: 10px; `; +const LoginRequiredTextWrapper = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; + const categoryTab: CategoryItemType[] = [ - { id: 'HOST', label: '모집중' }, - { id: 'VOLUNTEER', label: '참가중' }, + { id: 'VOLUNTEER', label: '참여' }, + { id: 'HOST', label: '개설' }, ]; const statusTab: StatusItemType[] = [ { id: 'RECRUIT', label: '모집중' }, { id: 'RECRUIT_FINISH', - label: '모집완료', + label: '모집마감', }, - { id: 'PARTY_FINISH', label: '파티종료' }, + { id: 'PARTY_FINISH', label: '종료' }, ]; -function isPartySituationRole(value: unknown): value is PartySituationRole { +function isPartyRole(value: unknown): value is PartyStatusRole { return value === 'HOST' || value === 'VOLUNTEER'; } function isPartySituation(value: unknown): value is PartyCurrentSituationRequestStatus { @@ -67,21 +74,21 @@ function isPartySituation(value: unknown): value is PartyCurrentSituationRequest const PartySituation = () => { const { replace, query } = useRouter(); - const situationRole = useSearchParam('role'); - const situation = useSearchParam('situation'); + const role = useSearchParam('role'); + const status = useSearchParam('status'); const selectedRole = useMemo(() => { - if (!situationRole || !isPartySituationRole(situationRole)) { + if (!role || !isPartyRole(role)) { return; } - return situationRole; - }, [situationRole]); + return role; + }, [role]); - const selectedSituation = useMemo(() => { - if (!situation || !isPartySituation(situation)) { + const selectedStatus = useMemo(() => { + if (!status || !isPartySituation(status)) { return; } - return situation; - }, [situation]); + return status; + }, [status]); return ( @@ -112,7 +119,7 @@ const PartySituation = () => { ); @@ -121,8 +128,26 @@ const PartySituation = () => { - - + { + if (error?.response?.status === 401) { + return ( + + + + ); + } + }} + > + diff --git a/src/components/profile/party/status/PartySituationItemList.tsx b/src/components/profile/party/status/PartySituationItemList.tsx new file mode 100644 index 0000000..a30f7de --- /dev/null +++ b/src/components/profile/party/status/PartySituationItemList.tsx @@ -0,0 +1,67 @@ +import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { FC } from 'react'; +import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyCurrentSituation'; +import { PartyStatusRole } from './PartySituation'; +import PartyList from '../PartyList'; +import { DefaultText } from '@components/common/DefaultText'; +import styled from '@emotion/styled'; +import { PartyCurrentSituationRequestStatus } from 'types/party'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; + +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 statusList = useSuspenseQuery({ + // queryKey: [API_GET_PARTY_STATUS_KEY, { role: selectedRole }], + // queryFn: () => getPartyStatus({ role: selectedRole }), + // }); + + 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) => lastPage?.pageInfo?.page, + }); + 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/ProfileReviewList.tsx b/src/components/profile/review/ProfileReviewList.tsx similarity index 96% rename from src/components/profile/ProfileReviewList.tsx rename to src/components/profile/review/ProfileReviewList.tsx index f04e86a..0f00c91 100644 --- a/src/components/profile/ProfileReviewList.tsx +++ b/src/components/profile/review/ProfileReviewList.tsx @@ -2,16 +2,16 @@ 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 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 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'; +import ReviewCard from '../../common/card/ReviewCard'; interface ProfileReviewListProps {} type ProfileReviewListType = '보낸리뷰' | '받은리뷰'; 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; From 3864e6c1eecc0cbc1c7cd5dae3c2da6bbb1fe9b0 Mon Sep 17 00:00:00 2001 From: minchodang Date: Sun, 28 Apr 2024 14:02:49 +0900 Subject: [PATCH 05/14] =?UTF-8?q?Fix:=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20api=20=ED=83=80=EC=9E=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20mockdata=20=EC=B2=98=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/profile/index.tsx | 3 +- src/api/getProfileReviewList.ts | 21 --- src/api/getReviewList.ts | 23 +++ src/components/common/card/ReviewCard.tsx | 12 +- src/components/profile/ProfileTab.tsx | 4 +- .../party/status/PartySituationItemList.tsx | 5 - .../profile/review/ProfileReview.tsx | 177 ++++++++++++++++++ .../profile/review/ProfileReviewItemList.tsx | 60 ++++++ .../profile/review/ProfileReviewList.tsx | 143 -------------- types/review/index.ts | 2 +- 10 files changed, 273 insertions(+), 177 deletions(-) delete mode 100644 src/api/getProfileReviewList.ts create mode 100644 src/api/getReviewList.ts create mode 100644 src/components/profile/review/ProfileReview.tsx create mode 100644 src/components/profile/review/ProfileReviewItemList.tsx delete mode 100644 src/components/profile/review/ProfileReviewList.tsx diff --git a/pages/profile/index.tsx b/pages/profile/index.tsx index 8baaaa1..bb6ebd8 100644 --- a/pages/profile/index.tsx +++ b/pages/profile/index.tsx @@ -36,6 +36,7 @@ const ProfileInfoContainer = styled.div` const RightAreaContainer = styled.div` display: flex; height: 100%; + justify-content: flex-end; padding: 0 8px; align-items: center; cursor: pointer; @@ -104,7 +105,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, - destination: '/profile?category=situation&role=HOST&status=RECRUIT', + destination: '/profile?category=situation&role=VOLUNTEER&status=RECRUIT', }, }; } 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/profile/ProfileTab.tsx b/src/components/profile/ProfileTab.tsx index a209254..a25ccb9 100644 --- a/src/components/profile/ProfileTab.tsx +++ b/src/components/profile/ProfileTab.tsx @@ -5,8 +5,8 @@ import { useSearchParam } from 'react-use'; import PartySituation from './party/status/PartySituation'; import TabComponent from './TabComponent'; import PartyRequest from './party/request/PartyRequest'; -import ReviewList from './review/ProfileReviewList'; -import ProfileReviewList from './review/ProfileReviewList'; +import ReviewList from './review/ProfileReview'; +import ProfileReviewList from './review/ProfileReview'; interface CategoryItemType { id: CategoryIdType; diff --git a/src/components/profile/party/status/PartySituationItemList.tsx b/src/components/profile/party/status/PartySituationItemList.tsx index a30f7de..a0f15ba 100644 --- a/src/components/profile/party/status/PartySituationItemList.tsx +++ b/src/components/profile/party/status/PartySituationItemList.tsx @@ -25,11 +25,6 @@ const PartySituationItemList: FC = ({ selectedRole, selectedStatus, }) => { - // const statusList = useSuspenseQuery({ - // queryKey: [API_GET_PARTY_STATUS_KEY, { role: selectedRole }], - // queryFn: () => getPartyStatus({ role: selectedRole }), - // }); - const partyStatusList = useSuspenseInfiniteQuery({ queryKey: [API_GET_PARTY_STATUS_KEY, , { role: selectedRole }], queryFn: ({ pageParam = 0 }) => diff --git a/src/components/profile/review/ProfileReview.tsx b/src/components/profile/review/ProfileReview.tsx new file mode 100644 index 0000000..7495059 --- /dev/null +++ b/src/components/profile/review/ProfileReview.tsx @@ -0,0 +1,177 @@ +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 { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; +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'; +import getReviewList, { + API_GET_REVIEW_LIST_KEY, + ReviewListRequestType, +} from 'src/api/getReviewList'; +import { DefaultText } from '@components/common/DefaultText'; +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]); + + // // 랜덤 이미지를 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: () => getProfileReview({ reviewType: selectedRole ?? 'RECEIVER' }), + // // }); + + // // console.log(reviewData); + + const reviewList = useSuspenseInfiniteQuery({ + queryKey: [API_GET_REVIEW_LIST_KEY, , { role: selectedRole }], + queryFn: ({ pageParam = 0 }) => + getReviewList({ + page: pageParam, + reviewType: selectedRole ?? 'RECEIVER', + size: 5, + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, + }); + + 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..378ba70 --- /dev/null +++ b/src/components/profile/review/ProfileReviewItemList.tsx @@ -0,0 +1,60 @@ +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) => lastPage?.pageInfo?.page, + }); + + 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/profile/review/ProfileReviewList.tsx b/src/components/profile/review/ProfileReviewList.tsx deleted file mode 100644 index 0f00c91..0000000 --- a/src/components/profile/review/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/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; } From 25cd05b575051fc2ed2c7f65e6f45a5ae6255c57 Mon Sep 17 00:00:00 2001 From: minchodang Date: Wed, 1 May 2024 10:56:10 +0900 Subject: [PATCH 06/14] =?UTF-8?q?Fix:=20getNextPageParam=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20searchInput=20defa?= =?UTF-8?q?ultValue=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/getPartyMainPage.ts | 2 +- src/components/home/HomeList.tsx | 11 ++- .../party/request/PartyRequestItemList.tsx | 18 +++-- .../party/status/PartySituationItemList.tsx | 21 +++-- .../profile/review/ProfileReview.tsx | 79 +------------------ .../profile/review/ProfileReviewItemList.tsx | 9 ++- src/components/search/SearchResult.tsx | 7 +- src/components/search/header/index.tsx | 13 ++- 8 files changed, 62 insertions(+), 98 deletions(-) diff --git a/src/api/getPartyMainPage.ts b/src/api/getPartyMainPage.ts index 8e28eea..65d4830 100644 --- a/src/api/getPartyMainPage.ts +++ b/src/api/getPartyMainPage.ts @@ -5,7 +5,7 @@ import { PartyListResponse } from 'types/common/PartyListResponse'; interface GetMainPageParameter { longitude: number; latitude: number; - lastPartyId?: number; + page?: number; size?: number; } diff --git a/src/components/home/HomeList.tsx b/src/components/home/HomeList.tsx index c17a437..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?.page, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, }); const onClickPartyCard = (id: number) => { diff --git a/src/components/profile/party/request/PartyRequestItemList.tsx b/src/components/profile/party/request/PartyRequestItemList.tsx index 281d3da..9cc0aab 100644 --- a/src/components/profile/party/request/PartyRequestItemList.tsx +++ b/src/components/profile/party/request/PartyRequestItemList.tsx @@ -1,12 +1,11 @@ -import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; +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 PartyRequestList from './PartyRequestCard'; -import { DefaultText } from '@components/common/DefaultText'; -import styled from '@emotion/styled'; import PartyRequestCard from './PartyRequestCard'; -import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; interface PartyRequestItemListProps { role: PartyRequestRole; @@ -22,7 +21,7 @@ const Container = styled.div` const PartyRequestItemList: FC = ({ role }) => { const partyRequestList = useSuspenseInfiniteQuery({ - queryKey: [API_GET_PARTY_JOIN_KEY, , { role }], + queryKey: [API_GET_PARTY_JOIN_KEY, { role }], queryFn: ({ pageParam = 0 }) => getPartyJoin({ page: pageParam, @@ -30,7 +29,12 @@ const PartyRequestItemList: FC = ({ role }) => { size: 5, }), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, }); const onObserve = () => { if (partyRequestList.hasNextPage) partyRequestList.fetchNextPage(); diff --git a/src/components/profile/party/status/PartySituationItemList.tsx b/src/components/profile/party/status/PartySituationItemList.tsx index a0f15ba..8dcdbfe 100644 --- a/src/components/profile/party/status/PartySituationItemList.tsx +++ b/src/components/profile/party/status/PartySituationItemList.tsx @@ -1,12 +1,12 @@ -import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; -import { FC } from 'react'; -import getPartyStatus, { API_GET_PARTY_STATUS_KEY } from 'src/api/getPartyCurrentSituation'; -import { PartyStatusRole } from './PartySituation'; -import PartyList from '../PartyList'; 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 { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import PartyList from '../PartyList'; +import { PartyStatusRole } from './PartySituation'; interface PartySituationItemListProps { selectedRole: PartyStatusRole; @@ -26,7 +26,7 @@ const PartySituationItemList: FC = ({ selectedStatus, }) => { const partyStatusList = useSuspenseInfiniteQuery({ - queryKey: [API_GET_PARTY_STATUS_KEY, , { role: selectedRole }], + queryKey: [API_GET_PARTY_STATUS_KEY, { role: selectedRole }], queryFn: ({ pageParam = 0 }) => getPartyStatus({ page: pageParam, @@ -35,7 +35,12 @@ const PartySituationItemList: FC = ({ size: 5, }), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, }); const onObserve = () => { if (partyStatusList.hasNextPage) partyStatusList.fetchNextPage(); diff --git a/src/components/profile/review/ProfileReview.tsx b/src/components/profile/review/ProfileReview.tsx index 7495059..7cad5c5 100644 --- a/src/components/profile/review/ProfileReview.tsx +++ b/src/components/profile/review/ProfileReview.tsx @@ -1,21 +1,11 @@ +import { DefaultText } from '@components/common/DefaultText'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; -import { FC, useMemo } from 'react'; +import React, { FC, useMemo } from 'react'; import { useSearchParam } from 'react-use'; +import { ReviewListRequestType } from 'src/api/getReviewList'; import ProfileTabSortingButton from '../ProfileTabSortingButton'; -import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; -import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'; -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'; -import getReviewList, { - API_GET_REVIEW_LIST_KEY, - ReviewListRequestType, -} from 'src/api/getReviewList'; -import { DefaultText } from '@components/common/DefaultText'; import ProfileReviewItemList from './ProfileReviewItemList'; interface ProfileReviewProps {} @@ -70,67 +60,6 @@ const ProfileReview: FC = () => { 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: () => getProfileReview({ reviewType: selectedRole ?? 'RECEIVER' }), - // // }); - - // // console.log(reviewData); - - const reviewList = useSuspenseInfiniteQuery({ - queryKey: [API_GET_REVIEW_LIST_KEY, , { role: selectedRole }], - queryFn: ({ pageParam = 0 }) => - getReviewList({ - page: pageParam, - reviewType: selectedRole ?? 'RECEIVER', - size: 5, - }), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, - }); - return ( diff --git a/src/components/profile/review/ProfileReviewItemList.tsx b/src/components/profile/review/ProfileReviewItemList.tsx index 378ba70..714f74d 100644 --- a/src/components/profile/review/ProfileReviewItemList.tsx +++ b/src/components/profile/review/ProfileReviewItemList.tsx @@ -23,7 +23,7 @@ const Container = styled.div` const ProfileReviewItemList: FC = ({ role }) => { const reviewList = useSuspenseInfiniteQuery({ - queryKey: [API_GET_REVIEW_LIST_KEY, , { role }], + queryKey: [API_GET_REVIEW_LIST_KEY, { role }], queryFn: ({ pageParam = 0 }) => getReviewList({ page: pageParam, @@ -31,7 +31,12 @@ const ProfileReviewItemList: FC = ({ role }) => { size: 5, }), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, }); const onObserve = () => { diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx index 2c24db6..40b92a0 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/search/SearchResult.tsx @@ -35,7 +35,12 @@ export const SearchResult: FC = ({ keyword }) => { lastPartyId: pageParam, }), initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage?.pageInfo?.page, + getNextPageParam: (lastPage) => { + if (!lastPage?.pageInfo.hasNext) { + return undefined; + } + return lastPage.pageInfo.page + 1; + }, staleTime: 0, }); 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 ( - + ); }; From 1ffa78aaad8b1b06c8bcb63f8d519a90f5f84b11 Mon Sep 17 00:00:00 2001 From: minchodang Date: Wed, 1 May 2024 00:15:59 +0900 Subject: [PATCH 07/14] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20api=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/deleteReview.ts | 15 +++++++++++++++ src/api/getHostReviewList.ts | 20 ++++++++++++++++++++ src/api/getReviewDetail.ts | 20 ++++++++++++++++++++ src/api/patchReview.ts | 14 ++++++++++++++ src/api/postReview.ts | 14 ++++++++++++++ types/review/index.ts | 10 ++++++++++ 6 files changed, 93 insertions(+) create mode 100644 src/api/deleteReview.ts create mode 100644 src/api/getHostReviewList.ts create mode 100644 src/api/getReviewDetail.ts create mode 100644 src/api/patchReview.ts create mode 100644 src/api/postReview.ts diff --git a/src/api/deleteReview.ts b/src/api/deleteReview.ts new file mode 100644 index 0000000..a3f33be --- /dev/null +++ b/src/api/deleteReview.ts @@ -0,0 +1,15 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; + +export interface DeleteReviewBody { + reviewId: number; +} + +const deleteReview = async (body: DeleteReviewBody) => { + return defaultRequest.delete('/api/review', { + data: { + body, + }, + }); +}; + +export default deleteReview; diff --git a/src/api/getHostReviewList.ts b/src/api/getHostReviewList.ts new file mode 100644 index 0000000..d0d138f --- /dev/null +++ b/src/api/getHostReviewList.ts @@ -0,0 +1,20 @@ +import variableAssignment from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { GetReviewListResponse } from 'types/review'; + +interface GetHostReviewListParameter { + page: number; + size: number; + hostId: number; +} + +export const API_GET_HOST_REVIEW_LIST = `/api/review/host`; + +export const getHostReviewList = async (params: GetHostReviewListParameter) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'reviewGetResList', GetReviewListResponse> + >(API_GET_HOST_REVIEW_LIST, { + params, + }); + return data; +}; diff --git a/src/api/getReviewDetail.ts b/src/api/getReviewDetail.ts new file mode 100644 index 0000000..39785e9 --- /dev/null +++ b/src/api/getReviewDetail.ts @@ -0,0 +1,20 @@ +import variableAssignment from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { GetReviewDetailResponse } from 'types/review'; + +interface GetReviewDetailParameter { + reviewId: string; +} + +export const API_GET_REVIEW_DETAIL = `/api/review/{{reviewId}}`; + +const getReviewDetail = async ({ reviewId }: GetReviewDetailParameter) => { + const { data } = await defaultRequest.get( + variableAssignment(API_GET_REVIEW_DETAIL, { + reviewId, + }), + ); + return data; +}; + +export default getReviewDetail; diff --git a/src/api/patchReview.ts b/src/api/patchReview.ts new file mode 100644 index 0000000..a4d5e72 --- /dev/null +++ b/src/api/patchReview.ts @@ -0,0 +1,14 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; + +export interface PatchReviewBody { + reviewId: number; + content: string; + rating: number; + imgUrl?: string[]; +} + +const patchReview = async (body: PatchReviewBody) => { + return defaultRequest.patch('/api/review', body); +}; + +export default patchReview; diff --git a/src/api/postReview.ts b/src/api/postReview.ts new file mode 100644 index 0000000..a6b5121 --- /dev/null +++ b/src/api/postReview.ts @@ -0,0 +1,14 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; + +export interface PostReviewBody { + reviewId: number; + content: string; + rating: number; + imgUrl?: string[]; +} + +const postReview = async (body: PostReviewBody) => { + return defaultRequest.post('/api/review', body); +}; + +export default postReview; diff --git a/types/review/index.ts b/types/review/index.ts index 5e6f9f9..630531c 100644 --- a/types/review/index.ts +++ b/types/review/index.ts @@ -12,3 +12,13 @@ export interface GetReviewListResponse { reviewImg: string[]; createdAt: string | Date; } + +export interface GetReviewDetailResponse { + reviewId: number; + nickname: string; + rating: number; + content: string; + reviewImg: string[]; + createdAt: string | Date; + isSelfReview: boolean; +} From c05e15dc53aefdea725c8cbd638e434a36394ac2 Mon Sep 17 00:00:00 2001 From: minchodang Date: Wed, 1 May 2024 16:20:34 +0900 Subject: [PATCH 08/14] =?UTF-8?q?Fix:=20=ED=8C=8C=ED=8B=B0=EC=B0=B8?= =?UTF-8?q?=EA=B0=80=20=EC=8B=A0=EC=B2=AD=20=ED=8C=9D=EC=97=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/[id].tsx | 47 ++- src/api/getPartyDetail.ts | 22 +- src/api/postParticipate.ts | 18 +- src/components/common/DefaultHeader.tsx | 2 +- src/components/common/Toast.tsx | 83 +++--- src/components/partydetail/PartyBrief.tsx | 196 ++++++------- src/components/partydetail/PartyDetail.tsx | 93 +++--- .../partydetail/PartyDetailBottomBar.tsx | 269 +++++++++++------- .../partydetail/PartyDetailContent.tsx | 91 ++---- .../PartyDetailPartyRequestPopup.tsx | 82 ++++++ src/components/partydetail/PartyInfo.tsx | 105 ++++--- src/components/partydetail/PartyMap.tsx | 61 ++-- src/components/popup/ConfirmPopup.tsx | 110 ++++--- 13 files changed, 620 insertions(+), 559 deletions(-) create mode 100644 src/components/partydetail/PartyDetailPartyRequestPopup.tsx diff --git a/pages/party/[id].tsx b/pages/party/[id].tsx index 49d5abc..5d91d0c 100644 --- a/pages/party/[id].tsx +++ b/pages/party/[id].tsx @@ -1,33 +1,30 @@ -import styled from "@emotion/styled"; -import { DefaultHeader } from "@components/common/DefaultHeader"; -import { HeaderBackButton } from "@components/common/HeaderBackButton"; -import { useScroll } from "react-use"; -import { useRef } from "react"; -import PartyDetailContent from "@components/partydetail/PartyDetailContent"; -import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; +import styled from '@emotion/styled'; +import { DefaultHeader } from '@components/common/DefaultHeader'; +import { HeaderBackButton } from '@components/common/HeaderBackButton'; +import { useScroll } from 'react-use'; +import { useRef } from 'react'; +import PartyDetailContent from '@components/partydetail/PartyDetailContent'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - margin: 0 auto; - height: 100%; - width: 100%; - max-width: 768px; - overflow-y: scroll; + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; + height: 100%; + width: 100%; + max-width: 768px; `; const PartyDetail = () => { - const scrollRef = useRef(null); - const { y } = useScroll(scrollRef); - return ( - - } /> - - - - - ); + return ( + + } /> + + + + + ); }; export default PartyDetail; diff --git a/src/api/getPartyDetail.ts b/src/api/getPartyDetail.ts index d84ce4c..48a8339 100644 --- a/src/api/getPartyDetail.ts +++ b/src/api/getPartyDetail.ts @@ -1,22 +1,16 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; interface GetPartyDetailParameter { - id: string; - userId: string; + id: string; } -export const API_GET_PARTY_DETAIL_KEY = "/api/party/{{id}}?userId={{userId}}"; +export const API_GET_PARTY_DETAIL_KEY = '/api/party/{{id}}'; -const getPartyDetail = async ({ - id, - userId, -}: GetPartyDetailParameter): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_PARTY_DETAIL_KEY, { id, userId }) - ); - return data; +const getPartyDetail = async ({ id }: GetPartyDetailParameter): Promise => { + const { data } = await defaultRequest.get(variableAssignMent(API_GET_PARTY_DETAIL_KEY, { id })); + return data; }; export default getPartyDetail; diff --git a/src/api/postParticipate.ts b/src/api/postParticipate.ts index 9dae0e8..4d17654 100644 --- a/src/api/postParticipate.ts +++ b/src/api/postParticipate.ts @@ -1,15 +1,17 @@ -import defaultRequest from "src/lib/axios/defaultRequest"; +import defaultRequest from 'src/lib/axios/defaultRequest'; -interface postParticipateParameter { - partyId: number; - leaderId?: number; - status: string; +export type PostParticipateStatus = 'APPLY' | 'CANCEL'; + +interface PostParticipateParameter { + partyId: number; + oneLineIntroduce: string; + status: PostParticipateStatus; } -export const API_POST_PARTY_PARTICIPATION_KEY = "/api/party/participation"; +export const API_POST_PARTY_PARTICIPATION_KEY = '/api/party/participation'; -const postParticipate = async (body: postParticipateParameter) => { - return defaultRequest.post(API_POST_PARTY_PARTICIPATION_KEY, body); +const postParticipate = async (body: PostParticipateParameter) => { + return defaultRequest.post(API_POST_PARTY_PARTICIPATION_KEY, body); }; export default postParticipate; diff --git a/src/components/common/DefaultHeader.tsx b/src/components/common/DefaultHeader.tsx index 20cf504..085b587 100644 --- a/src/components/common/DefaultHeader.tsx +++ b/src/components/common/DefaultHeader.tsx @@ -13,9 +13,9 @@ const Wrapper = styled.div` background: ${ColorToken.white}; width: 100%; position: fixed; + z-index: 3; top: 0; left: 0; - z-index: 999; height: 45px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), diff --git a/src/components/common/Toast.tsx b/src/components/common/Toast.tsx index 2481dd2..409442c 100644 --- a/src/components/common/Toast.tsx +++ b/src/components/common/Toast.tsx @@ -1,60 +1,61 @@ -import styled from "@emotion/styled"; -import { ToastOption } from "types/toast"; -import InfoIcon from "@components/icons/toast/Info.icon"; -import WarnIcon from "@components/icons/toast/Warn.icon"; -import CloseIcon from "@components/icons/common/Close.icon"; +import styled from '@emotion/styled'; +import { ToastOption } from 'types/toast'; +import InfoIcon from '@components/icons/toast/Info.icon'; +import WarnIcon from '@components/icons/toast/Warn.icon'; +import CloseIcon from '@components/icons/common/Close.icon'; interface ToastIconProps { - type?: "info" | "warn"; + type?: 'info' | 'warn'; } interface ToastProps { - message: string; - option: ToastOption; - onToastClose: () => void; + message: string; + option: ToastOption; + onToastClose: () => void; } const Container = styled.div` - display: flex; - justify-content: space-between; - width: 320px; - height: 64px; - padding: 20px 12px; - align-items: center; - border-radius: 4px; - background: var(--neutral-white, #fff); - box-shadow: 0px 12px 24px 0px rgba(0, 0, 0, 0.2); + display: flex; + justify-content: space-between; + width: 320px; + height: 64px; + padding: 20px 12px; + align-items: center; + border-radius: 4px; + background: var(--neutral-white, #fff); + box-shadow: 0px 12px 24px 0px rgba(0, 0, 0, 0.2); + z-index: 5; `; const Content = styled.div` - display: flex; - align-items: center; - gap: 8px; + display: flex; + align-items: center; + gap: 8px; `; const ToastIcon = ({ type }: ToastIconProps) => { - switch (type) { - case "info": - return ; - case "warn": - return ; - default: - return null; - } + switch (type) { + case 'info': + return ; + case 'warn': + return ; + default: + return null; + } }; const Toast = ({ message, option, onToastClose }: ToastProps) => { - const { type } = option; - return ( - - - - {message} - - - - ); + const { type } = option; + return ( + + + + {message} + + + + ); }; export default Toast; diff --git a/src/components/partydetail/PartyBrief.tsx b/src/components/partydetail/PartyBrief.tsx index d66509b..46f04f9 100644 --- a/src/components/partydetail/PartyBrief.tsx +++ b/src/components/partydetail/PartyBrief.tsx @@ -1,135 +1,111 @@ -import styled from "@emotion/styled"; -import { DefaultText } from "@components/common/DefaultText"; -import ViewcountIcon from "@components/icons/profile/Viewcount.icon"; -import Divider from "@mui/material/Divider"; -import PersonIcon from "@components/icons/profile/Person.icon"; -import InfoIcon from "@components/icons/profile/Info.icon"; -import GenderIcon from "@components/icons/profile/Gender.icon"; -import { Fragment } from "react"; -import RestaurantIcon from "@components/icons/profile/Restaurant.icon"; -import { - PARTY_AGE_LABEL, - PARTY_GENDER_LABEL, - PARTY_CATEGORY_LABEL, -} from "src/constants/options"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; +import styled from '@emotion/styled'; +import { DefaultText } from '@components/common/DefaultText'; +import ViewcountIcon from '@components/icons/profile/Viewcount.icon'; +import Divider from '@mui/material/Divider'; +import PersonIcon from '@components/icons/profile/Person.icon'; +import InfoIcon from '@components/icons/profile/Info.icon'; +import GenderIcon from '@components/icons/profile/Gender.icon'; +import { Fragment } from 'react'; +import RestaurantIcon from '@components/icons/profile/Restaurant.icon'; +import { PARTY_AGE_LABEL, PARTY_GENDER_LABEL, PARTY_CATEGORY_LABEL } from 'src/constants/options'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; type PartyBriefProps = Pick< - PartyDetailResponse, - | "partyTitle" - | "hit" - | "totalParticipant" - | "participate" - | "gender" - | "age" - | "category" + PartyDetailResponse, + 'partyTitle' | 'hit' | 'totalParticipant' | 'participate' | 'gender' | 'age' | 'category' >; const Container = styled.div` - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px 20px; - background-color: white; - border-radius: 12px; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 20px; + background-color: white; + border-radius: 12px; `; const PartyTitle = styled.div` - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - z-index: 99; - gap: 10px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; `; const ViewcountContainer = styled.div` - display: flex; - flex-direction: row; - gap: 16px; + display: flex; + flex-direction: row; + gap: 16px; `; const PartyConditionContainer = styled.div` - display: flex; - flex-direction: row; - gap: 8px; - align-items: center; - justify-content: center; - font-size: 16px; + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + justify-content: center; + font-size: 16px; `; const PartyConditionBox = styled.div` - width: 80px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - z-index: 99; - gap: 10px; + width: 80px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 10px; `; const PartyBrief = (data: PartyBriefProps) => { - const { - partyTitle, - hit, - totalParticipant, - participate, - gender, - age, - category, - } = data; + const { partyTitle, hit, totalParticipant, participate, gender, age, category } = data; - const PartyBriefData = [ - { - id: "participant_count", - icon: , - content: `${String(participate)}/${String(totalParticipant)}명`, - }, - { - id: "gender_data", - icon: , - content: `${ - PARTY_GENDER_LABEL.find((item) => item.value === gender)?.name - }`, - }, - { - id: "age_data", - icon: , - content: `${PARTY_AGE_LABEL.find((item) => item.value === age)?.name}`, - }, - { - id: "category_data", - icon: , - content: `${ - PARTY_CATEGORY_LABEL.find((item) => item.value === category)?.name - }`, - }, - ]; + const PartyBriefData = [ + { + id: 'participant_count', + icon: , + content: `${String(participate)}/${String(totalParticipant)}명`, + }, + { + id: 'gender_data', + icon: , + content: `${PARTY_GENDER_LABEL.find((item) => item.value === gender)?.name}`, + }, + { + id: 'age_data', + icon: , + content: `${PARTY_AGE_LABEL.find((item) => item.value === age)?.name}`, + }, + { + id: 'category_data', + icon: , + content: `${PARTY_CATEGORY_LABEL.find((item) => item.value === category)?.name}`, + }, + ]; - return ( - - - - - - - - - - - {PartyBriefData.map(({ id, icon, content }, index) => ( - - {index !== 0 && } - - {icon} - - - - ))} - - - ); + return ( + + + + + + + + + + + {PartyBriefData.map(({ id, icon, content }, index) => ( + + {index !== 0 && } + + {icon} + + + + ))} + + + ); }; export default PartyBrief; diff --git a/src/components/partydetail/PartyDetail.tsx b/src/components/partydetail/PartyDetail.tsx index 799917a..f8cf0e4 100644 --- a/src/components/partydetail/PartyDetail.tsx +++ b/src/components/partydetail/PartyDetail.tsx @@ -1,67 +1,66 @@ -import styled from "@emotion/styled"; -import { DefaultText } from "@components/common/DefaultText"; -import Divider from "@mui/material/Divider"; -import dayjs from "dayjs"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; +import styled from '@emotion/styled'; +import { DefaultText } from '@components/common/DefaultText'; +import Divider from '@mui/material/Divider'; +import dayjs from 'dayjs'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; type PartyDetailProps = Pick< - PartyDetailResponse, - "deadline" | "partyTime" | "menu" | "partyContent" + PartyDetailResponse, + 'deadline' | 'partyTime' | 'menu' | 'partyContent' >; const Container = styled.div` - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px 20px; - z-index: 99; - background-color: white; - border-radius: 12px; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 20px; + background-color: white; + border-radius: 12px; `; const PartyDetailBox = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; + display: flex; + flex-direction: row; + justify-content: space-between; `; const PartyIntroduce = styled.div` - display: flex; - flex-direction: column; - gap: 16px; + display: flex; + flex-direction: column; + gap: 16px; `; const PartyDetail = (data: PartyDetailProps) => { - const { deadline, partyTime, menu, partyContent } = data; + const { deadline, partyTime, menu, partyContent } = data; - const partyDetailData = [ - { - title: "모집마감", - content: dayjs(deadline).format("YYYY.MM.MM. HH:MM"), - }, + const partyDetailData = [ + { + title: '모집마감', + content: dayjs(deadline).format('YYYY.MM.MM. HH:MM'), + }, - { - title: "모임시간", - content: dayjs(partyTime).format("YYYY.MM.MM. HH:MM"), - }, - { title: "식사메뉴", content: menu }, - ]; + { + title: '모임시간', + content: dayjs(partyTime).format('YYYY.MM.MM. HH:MM'), + }, + { title: '식사메뉴', content: menu }, + ]; - return ( - - {partyDetailData.map(({ title, content }) => ( - - - - - ))} - - - - - - - ); + return ( + + {partyDetailData.map(({ title, content }) => ( + + + + + ))} + + + + + + + ); }; export default PartyDetail; diff --git a/src/components/partydetail/PartyDetailBottomBar.tsx b/src/components/partydetail/PartyDetailBottomBar.tsx index cf1134a..140e7c6 100644 --- a/src/components/partydetail/PartyDetailBottomBar.tsx +++ b/src/components/partydetail/PartyDetailBottomBar.tsx @@ -1,127 +1,186 @@ -import styled from "@emotion/styled"; -import DeleteIcon from "@components/icons/common/Delete.icon"; -import EditIcon from "@components/icons/common/Edit.icon"; -import { DefaultButton } from "@components/common/DefaultButton"; -import { DefaultModalContainer } from "@components/common/DefaultModalContainer"; -import { Transition } from "@mantine/core"; -import { useState } from "react"; -import { MouseEventHandler } from "react"; -import ConfirmPopup from "../popup/ConfirmPopup"; +import { DefaultButton } from '@components/common/DefaultButton'; +import { DefaultModalContainer } from '@components/common/DefaultModalContainer'; +import styled from '@emotion/styled'; +import useToast from '@hooks/useToast'; +import { Transition } from '@mantine/core'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import deletePartyDetail from 'src/api/deletePartyDetail'; +import { API_GET_PARTY_DETAIL_KEY } from 'src/api/getPartyDetail'; +import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; +import postParticipate from 'src/api/postParticipate'; +import ConfirmPopup from '../popup/ConfirmPopup'; +import PartyDetailPartyRequestPopup from './PartyDetailPartyRequestPopup'; +import { isAxiosError } from 'axios'; +export interface PartyRequestPopupForm { + oneLineIntroduce: string; +} interface PartyDetailBottomBarProps { - participateParty: MouseEventHandler; - partyDetailDelete: MouseEventHandler; - isLeader: boolean; + isLeader: boolean; } const Container = styled.div` - width: 100%; - height: 75px; - display: flex; - justify-content: center; - align-items: center; - position: absolute; - position: fixed; - bottom: 0; - border-top: 1px solid #dddddd; - z-index: 99999999999999; - background-color: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); - transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + width: 100%; + height: 75px; + display: flex; + justify-content: center; + align-items: center; + bottom: 0; + border-top: 1px solid #dddddd; + position: fixed; + z-index: 3; + background-color: white; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); `; const IconContainer = styled.div` - width: 64px; - height: 64px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - cursor: pointer; - transition: all 0.1s; - &:hover { - background-color: #dddddd; - } + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + cursor: pointer; + transition: all 0.1s; + &:hover { + background-color: #dddddd; + } `; const BottomBarContainer = styled.div` - width: 768px; - display: flex; -`; - -const ParcitipateButtonContainer = styled.div` - display: flex; - width: 100%; - justify-content: center; + width: 768px; + display: flex; `; -const HostPannelContainer = styled.div` - display: flex; - width: 100%; - justify-content: right; +const ButtonContainer = styled.div` + display: flex; + width: 100%; + justify-content: center; + padding: 20px; + gap: 10px; `; -const PartyDetailBottomBar = ({ - participateParty, - partyDetailDelete, - isLeader, -}: PartyDetailBottomBarProps) => { - const [isOpen, setIsOpen] = useState(false); +const PartyDetailBottomBar = ({ isLeader }: PartyDetailBottomBarProps) => { + const { push } = useRouter(); + const [isOpenDeletePopup, setIsOpenDeletePopup] = useState(false); + const [isOpenParticipatePopup, setIsOpenParticipatePopup] = useState(false); + const { showToast } = useToast(); + const router = useRouter(); + const { id } = router.query as { id: string }; + const queryClient = useQueryClient(); + const form = useForm({ + defaultValues: { + oneLineIntroduce: '', + }, + }); - const OpenConfirmPopup = () => { - setIsOpen(true); - }; + const postParticipateMutate = useMutation({ + mutationFn: postParticipate, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], + }); + showToast('파티가 신청되었습니다. 방장의 수락을 기다려 주세요.'); + setIsOpenParticipatePopup(false); + }, + onError: (error) => { + if (isAxiosError(error)) { + console.log(error); + showToast(error.response?.data.errorMessage); + } + }, + }); - const CloseConfirmPopup = () => { - setIsOpen(false); - }; + const deletePartyDetailMutate = useMutation({ + mutationFn: deletePartyDetail, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [API_GET_MAIN_PAGE], + }); + showToast('파티가 삭제되었습니다.'); + router.push('/'); + }, + }); - const onClickEditHandler = () => { - //편집페이지 라우팅 및 데이터전달 - }; + const onValid = ({ oneLineIntroduce }: PartyRequestPopupForm) => { + postParticipateMutate.mutate({ + partyId: Number(id), + oneLineIntroduce, + status: 'APPLY', + }); + }; - return ( - - - {isLeader ? ( - - - - - - - - - ) : ( - - - - )} - - - {(styles) => ( - - - - )} - - - ); + return ( + + + {isLeader ? ( + + setIsOpenDeletePopup(true)} + /> + push(`/party/edit/${id}`)} + /> + + ) : ( + + setIsOpenParticipatePopup(true)} + /> + + )} + + + {(styles) => ( + + + { + setIsOpenParticipatePopup(false); + }} + onSubmit={form.handleSubmit(onValid)} + /> + + + )} + + + {(styles) => ( + + setIsOpenDeletePopup(false)} + confirmPopup={() => { + deletePartyDetailMutate.mutate({ id }); + }} + description="정말로 삭제하시겠습니까?" + /> + + )} + + + ); }; export default PartyDetailBottomBar; diff --git a/src/components/partydetail/PartyDetailContent.tsx b/src/components/partydetail/PartyDetailContent.tsx index e11aac9..747a1b7 100644 --- a/src/components/partydetail/PartyDetailContent.tsx +++ b/src/components/partydetail/PartyDetailContent.tsx @@ -1,74 +1,33 @@ -import PartyInfo from "@components/partydetail/PartyInfo"; -import { useRouter } from "next/router"; -import { useQuery } from "@tanstack/react-query"; -import getPartyDetail, { - API_GET_PARTY_DETAIL_KEY, -} from "src/api/getPartyDetail"; -import BackgroundImage from "@components/common/BackgroundImage"; -import PartyDetailBottomBar from "@components/partydetail/PartyDetailBottomBar"; -import postParticipate from "src/api/postParticipate"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import deletePartyDetail from "src/api/deletePartyDetail"; -import { API_GET_MAIN_PAGE } from "src/api/getPartyMainPage"; - -const PartyDetailContent = ({ y }: { y: number }) => { - const router = useRouter(); - const { id } = router.query as { id: string }; - const queryClient = useQueryClient(); - const userId = 11; - - const { data } = useQuery({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - queryFn: () => getPartyDetail({ id, userId: String(userId) }), - enabled: !!id, - }); - - const postParticipateMutate = useMutation({ - mutationFn: postParticipate, - onSuccess: () => { - queryClient.invalidateQueries({ +import PartyInfo from '@components/partydetail/PartyInfo'; +import { useRouter } from 'next/router'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import getPartyDetail, { API_GET_PARTY_DETAIL_KEY } from 'src/api/getPartyDetail'; +import BackgroundImage from '@components/common/BackgroundImage'; +import PartyDetailBottomBar from '@components/partydetail/PartyDetailBottomBar'; +import postParticipate from 'src/api/postParticipate'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import deletePartyDetail from 'src/api/deletePartyDetail'; +import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; +import useToast from '@hooks/useToast'; +import styled from '@emotion/styled'; +import Image from 'next/image'; + +const PartyDetailContent = () => { + const router = useRouter(); + const { id } = router.query as { id: string }; + + const { data } = useSuspenseQuery({ queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - }); - router.push("/"); - }, - }); - - const DeletePartyDetailMutate = useMutation({ - mutationFn: deletePartyDetail, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - }); - router.push("/"); - }, - }); - - const participateParty = () => { - postParticipateMutate.mutate({ - partyId: Number(id), - status: "APPLY", + queryFn: () => getPartyDetail({ id }), }); - }; - - const partyDetailDelete = () => { - DeletePartyDetailMutate.mutate({ id }); - }; - return ( - <> - {data && ( + return ( <> - - - + 배경 이미지 + + - )} - - ); + ); }; export default PartyDetailContent; diff --git a/src/components/partydetail/PartyDetailPartyRequestPopup.tsx b/src/components/partydetail/PartyDetailPartyRequestPopup.tsx new file mode 100644 index 0000000..7d352e8 --- /dev/null +++ b/src/components/partydetail/PartyDetailPartyRequestPopup.tsx @@ -0,0 +1,82 @@ +import { DefaultButton } from '@components/common/DefaultButton'; +import { DefaultText } from '@components/common/DefaultText'; +import TextArea from '@components/common/TextArea'; +import styled from '@emotion/styled'; +import { FC, FormEventHandler } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { PartyRequestPopupForm } from './PartyDetailBottomBar'; + +interface PartyDetailPartyRequestPopupProps { + closePopup: VoidFunction; + onSubmit: FormEventHandler; +} + +const Container = styled.form` + display: flex; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; +`; +const TextSection = styled.div` + display: flex; + width: 100%; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 10px; +`; +const PartyDetailContainer = styled.div` + display: flex; + flex-direction: column; + padding: 40px; + width: 400px; + background-color: white; + border-radius: 12px; +`; +const ButtonContainer = styled.div` + display: flex; + width: 100%; + justify-content: space-around; + background-color: white; + gap: 10px; +`; + +const PartyDetailPartyRequestPopup: FC = ({ + closePopup, + onSubmit, +}) => { + const { register, control } = useFormContext(); + const oneLineIntroduce = useWatch({ control, name: 'oneLineIntroduce' }); + + return ( + + + + +