diff --git a/src/_apis/crew/crew-list.ts b/src/_apis/crew/crew-list.ts index f2719143..591912dc 100644 --- a/src/_apis/crew/crew-list.ts +++ b/src/_apis/crew/crew-list.ts @@ -5,16 +5,24 @@ export async function getCrewList(condition: ConditionTypes, pageable: PageableT const { keyword, mainLocation, mainCategory, subCategory, sortType } = condition; const { page, size, sort = ['string'] } = pageable; - const response: { data: MainCrewListResponse } = await fetchApi( - `/api/crews/search?keyword=${keyword}&mainLocation=${mainLocation}&mainCategory=${mainCategory}&subCategory=${subCategory}&sortType=${sortType}&page=${page}&size=${size}&sort=${sort}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', + try { + const response: { data: MainCrewListResponse } = await fetchApi( + `/api/crews/search?keyword=${keyword}&mainLocation=${mainLocation}&mainCategory=${mainCategory}&subCategory=${subCategory}&sortType=${sortType}&page=${page}&size=${size}&sort=${sort}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // 인증 정보를 요청에 포함 }, - credentials: 'include', // 인증 정보를 요청에 포함 - }, - ); - - return response?.data; + ); + if (!response.data) { + throw new Error('Failed to get crew list: No data received'); + } + return response.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error(`크루 목록 조회 실패`); + return null; + } } diff --git a/src/_apis/crew/my-crew-hosted-list.ts b/src/_apis/crew/my-crew-hosted-list.ts new file mode 100644 index 00000000..e55d990a --- /dev/null +++ b/src/_apis/crew/my-crew-hosted-list.ts @@ -0,0 +1,27 @@ +import { fetchApi } from '@/src/utils/api'; +import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card'; + +export async function getMyCrewHostedList(pageable: PageableTypes) { + const { page, size, sort = ['string'] } = pageable; + + try { + const response: { data: MyCrewListResponse } = await fetchApi( + `/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // 인증 정보를 요청에 포함 + }, + ); + if (!response.data) { + throw new Error('Failed to get my crew hosted list'); + } + return response.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error(`내가 개설한 크루 목록 조회 실패`); + return null; + } +} diff --git a/src/_apis/crew/my-crew-joined-list.ts b/src/_apis/crew/my-crew-joined-list.ts new file mode 100644 index 00000000..7679baea --- /dev/null +++ b/src/_apis/crew/my-crew-joined-list.ts @@ -0,0 +1,27 @@ +import { fetchApi } from '@/src/utils/api'; +import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card'; + +export async function getMyCrewJoinedList(pageable: PageableTypes) { + const { page, size, sort = ['string'] } = pageable; + + try { + const response: { data: MyCrewListResponse } = await fetchApi( + `/api/crews/joined?page=${page}&size=${size}&sort=${sort}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // 인증 정보를 요청에 포함 + }, + ); + if (!response.data) { + throw new Error('Failed to get my crew joined list'); + } + return response.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error(`내가 가입한 크루 목록 조회 실패`); + return null; + } +} diff --git a/src/_queries/crew/crew-list-queries.tsx b/src/_queries/crew/crew-list-queries.tsx index 6d2ce1f2..056b8dbd 100644 --- a/src/_queries/crew/crew-list-queries.tsx +++ b/src/_queries/crew/crew-list-queries.tsx @@ -1,7 +1,14 @@ +import { getCrewList } from '@/src/_apis/crew/crew-list'; import { ConditionTypes, MainCrewListResponse, PageableTypes } from '@/src/types/crew-card'; -import { getCrewList } from '../../_apis/crew/crew-list'; -export function useGetCrewListQuery(condition: ConditionTypes) { +export function useGetCrewListQuery({ + condition, + pageable, +}: { + condition: ConditionTypes; + pageable: PageableTypes; +}) { + const { size, sort = ['string'] } = pageable; return { queryKey: [ condition.keyword, @@ -11,14 +18,16 @@ export function useGetCrewListQuery(condition: ConditionTypes) { condition.sortType, ], queryFn: ({ pageParam = 0 }) => - getCrewList(condition, { page: pageParam, size: 6, sort: [condition.sortType] }).then( - (response) => { - if (response === undefined) { - throw new Error('Response is null'); - } - return response; - }, - ), + getCrewList(condition, { + page: pageParam, + size, + sort, + }).then((response) => { + if (response === undefined || response === null) { + throw new Error('Response is undefined'); + } + return response; + }), getNextPageParam: (lastPage: MainCrewListResponse, allPages: MainCrewListResponse[]) => lastPage.hasNext ? allPages.length : undefined, }; diff --git a/src/_queries/crew/my-crew-hosted-list-query.ts b/src/_queries/crew/my-crew-hosted-list-query.ts new file mode 100644 index 00000000..24ee37a6 --- /dev/null +++ b/src/_queries/crew/my-crew-hosted-list-query.ts @@ -0,0 +1,18 @@ +import { getMyCrewHostedList } from '@/src/_apis/crew/my-crew-hosted-list'; +import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card'; + +export function useGetMyCrewHostedQuery({ pageable }: { pageable: PageableTypes }) { + const { size, sort = ['string'] } = pageable; + return { + queryKey: ['myCrewHosted'], + queryFn: ({ pageParam = 0 }) => + getMyCrewHostedList({ page: pageParam, size, sort }).then((response) => { + if (response === undefined || response === null) { + throw new Error('크루 목록을 불러오는데 실패했습니다.'); + } + return response; + }), + getNextPageParam: (lastPage: MyCrewListResponse, allPages: MyCrewListResponse[]) => + lastPage.hasNext ? allPages.length : undefined, + }; +} diff --git a/src/_queries/crew/my-crew-joined-list-query.ts b/src/_queries/crew/my-crew-joined-list-query.ts new file mode 100644 index 00000000..0daced74 --- /dev/null +++ b/src/_queries/crew/my-crew-joined-list-query.ts @@ -0,0 +1,18 @@ +import { getMyCrewJoinedList } from '@/src/_apis/crew/my-crew-joined-list'; +import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card'; + +export function useGetMyCrewJoinedQuery({ pageable }: { pageable: PageableTypes }) { + const { size, sort = ['string'] } = pageable; + return { + queryKey: ['myCrewJoined'], + queryFn: ({ pageParam = 0 }) => + getMyCrewJoinedList({ page: pageParam, size, sort }).then((response) => { + if (response === undefined || response === null) { + throw new Error('크루 목록을 불러오는데 실패했습니다.'); + } + return response; + }), + getNextPageParam: (lastPage: MyCrewListResponse, allPages: MyCrewListResponse[]) => + lastPage.hasNext ? allPages.length : undefined, + }; +} diff --git a/src/app/(crew)/my-crew/hosted/page.tsx b/src/app/(crew)/my-crew/hosted/page.tsx new file mode 100644 index 00000000..69930ceb --- /dev/null +++ b/src/app/(crew)/my-crew/hosted/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Loader } from '@mantine/core'; +import { useGetMyCrewHostedQuery } from '@/src/_queries/crew/my-crew-hosted-list-query'; +import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; +import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; + +export default function MyCrewHostedPage() { + const { data, status, ref, isFetchingNextPage } = useInfiniteScroll( + useGetMyCrewHostedQuery({ + pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, + }), + ); + return ( +
+ + {status === 'pending' ? ( +
+ +
+ ) : ( +
+ )} + {status === 'error' &&

에러가 발생했습니다.

} +
+ ); +} diff --git a/src/app/(crew)/my-crew/joined/page.tsx b/src/app/(crew)/my-crew/joined/page.tsx new file mode 100644 index 00000000..0505ff9e --- /dev/null +++ b/src/app/(crew)/my-crew/joined/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Loader } from '@mantine/core'; +import { useGetMyCrewJoinedQuery } from '@/src/_queries/crew/my-crew-joined-list-query'; +import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; +import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; + +export default function MyCrewJoinedPage() { + const { data, status, ref, isFetchingNextPage } = useInfiniteScroll( + useGetMyCrewJoinedQuery({ + pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, + }), + ); + return ( +
+ + {status === 'pending' ? ( +
+ +
+ ) : ( +
+ )} + {status === 'error' &&

에러가 발생했습니다.

} +
+ ); +} diff --git a/src/app/(crew)/my-crew/layout.tsx b/src/app/(crew)/my-crew/layout.tsx new file mode 100644 index 00000000..0a3f593c --- /dev/null +++ b/src/app/(crew)/my-crew/layout.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { ReactNode, useEffect, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import Tabs from '@/src/components/common/tab'; + +export default function MyCrewLayout({ children }: { children: ReactNode }) { + const router = useRouter(); + const currentPath = usePathname(); + const myCrewTabs = [ + { label: '내가 참여한 크루', id: 'joined-crew', route: '/my-crew/joined' }, + { label: '내가 만든 크루', id: 'hosted-crew', route: '/my-crew/hosted' }, + ]; + const [currentTab, setCurrentTab] = useState(myCrewTabs[0].id); + + const handleTabClick = (id: string) => { + const targetRoute = myCrewTabs.find((tab) => tab.id === id)?.route; + if (targetRoute) router.push(targetRoute); + }; + + useEffect(() => { + const activeTabId = myCrewTabs.find((tab) => tab.route === currentPath)?.id; + if (activeTabId) setCurrentTab(activeTabId); + }, [currentPath]); + + return ( +
+
+ { + handleTabClick(id); + }} + /> +
+
{children}
+
+ ); +} diff --git a/src/app/(crew)/my-crew/page.tsx b/src/app/(crew)/my-crew/page.tsx index 976094b1..9322cc4d 100644 --- a/src/app/(crew)/my-crew/page.tsx +++ b/src/app/(crew)/my-crew/page.tsx @@ -1,40 +1,7 @@ 'use client'; -import { useState } from 'react'; -import Tabs from '@/src/components/common/tab'; +import { redirect } from 'next/navigation'; export default function MyCrewPage() { - const myPageTabs = [ - { label: '내가 참여한 크루', id: 'joined-crew' }, - { label: '내가 만든 크루', id: 'made-crew' }, - ]; - const [currentTab, setCurrentTab] = useState(myPageTabs[0].id); - - // TODO: fetchCrewData 함수를 사용하여 데이터를 불러오기 : 파라미터 수정 필요 - // TODO: 리스트와는 다른 데이터를 사용해야해서 우선 주석처리 했습니다. - // const { data, ref, isFetchingNextPage } = - // useInfiniteScroll(useGetCrewListQuery()); - - return ( -
-
- { - setCurrentTab(id); - }} - /> -
-
- {/* */} -
-
- ); + redirect('/my-crew/joined'); } diff --git a/src/app/(crew)/page.tsx b/src/app/(crew)/page.tsx index 5a4e86be..430424c6 100644 --- a/src/app/(crew)/page.tsx +++ b/src/app/(crew)/page.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from 'react'; import Image from 'next/image'; -import { Divider, TextInput } from '@mantine/core'; +import { Divider, Loader, TextInput } from '@mantine/core'; import { useGetCrewListQuery } from '@/src/_queries/crew/crew-list-queries'; import regionData from '@/src/data/region.json'; import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; @@ -10,7 +10,6 @@ import CategoryContainer from '@/src/app/_components/category/category-container import HeroCrew from '@/src/app/_components/hero/hero-crew'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; import DropDown from '@/src/components/common/input/drop-down'; -import { MainCrewListResponse } from '@/src/types/crew-card'; import IcoSearch from '@/public/assets/icons/ic-search.svg'; export default function HomePage() { @@ -35,13 +34,16 @@ export default function HomePage() { } }; - const { data, ref, isFetchingNextPage } = useInfiniteScroll( + const { data, status, isFetchingNextPage, ref } = useInfiniteScroll( useGetCrewListQuery({ - keyword: search, - mainLocation: handleRegionChange(region), - mainCategory, - subCategory, - sortType: sort === 'latest' ? 'LATEST' : 'POPULAR', + condition: { + keyword: search, + mainLocation: handleRegionChange(region), + mainCategory, + subCategory, + sortType: sort === 'latest' ? 'LATEST' : 'POPULAR', + }, + pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, }), ); @@ -124,6 +126,14 @@ export default function HomePage() {
{data && } + {status === 'pending' ? ( +
+ +
+ ) : ( +
+ )} + {status === 'error' &&

에러가 발생했습니다.

}
); diff --git a/src/app/_components/hero/hero-crew.tsx b/src/app/_components/hero/hero-crew.tsx index 893aa3b6..38a24889 100644 --- a/src/app/_components/hero/hero-crew.tsx +++ b/src/app/_components/hero/hero-crew.tsx @@ -1,7 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; import { useAuthStore } from '@/src/store/use-auth-store'; -import Button from '@/src/components/common/input/button'; import ImgHeroCrew from '@/public/assets/icons/ic-dumbbell.svg'; export default function HeroCrew() { diff --git a/src/components/common/crew-list/crew-card-list.stories.tsx b/src/components/common/crew-list/crew-card-list.stories.tsx index 4e8952f6..672875e0 100644 --- a/src/components/common/crew-list/crew-card-list.stories.tsx +++ b/src/components/common/crew-list/crew-card-list.stories.tsx @@ -1,10 +1,7 @@ -import { useEffect, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { InfiniteData } from '@tanstack/react-query'; import { useGetCrewListQuery } from '@/src/_queries/crew/crew-list-queries'; import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; import ClientProvider from '@/src/components/client-provider'; -import { MainCrewListResponse } from '@/src/types/crew-card'; import CrewCardList from './crew-card-list'; const meta: Meta = { @@ -32,11 +29,14 @@ type Story = StoryObj; function RenderCrewCardList() { const { data, ref, isFetchingNextPage } = useInfiniteScroll( useGetCrewListQuery({ - keyword: '', - mainLocation: '', - mainCategory: '', - subCategory: '', - sortType: 'LATEST', + condition: { + keyword: '', + mainLocation: '', + mainCategory: '', + subCategory: '', + sortType: 'LATEST', + }, + pageable: { page: 0, size: 6, sort: ['createdAt,desc'] }, }), ); diff --git a/src/components/common/crew-list/crew-card-list.tsx b/src/components/common/crew-list/crew-card-list.tsx index e5ca247d..8fb36641 100644 --- a/src/components/common/crew-list/crew-card-list.tsx +++ b/src/components/common/crew-list/crew-card-list.tsx @@ -10,38 +10,22 @@ import { import CrewCard from './crew-card'; // CrewCardListProps 타입을 구분하여 정의 -interface MainCrewCardListProps { - data: InfiniteData; +interface CrewCardListProps { + data: InfiniteData; isFetchingNextPage: boolean; - inWhere?: undefined; + inWhere?: 'my-crew' | 'main-crew'; } -interface MyCrewCardListProps { - data: InfiniteData; - isFetchingNextPage: boolean; - inWhere: 'my-crew'; -} - -// 유니온 타입으로 정의 -type CrewCardListProps = MainCrewCardListProps | MyCrewCardListProps; - -function CrewCardList( - { data, isFetchingNextPage, inWhere }: CrewCardListProps, - ref: React.Ref, -) { +function CrewCardList({ data, isFetchingNextPage, inWhere }: CrewCardListProps) { const crewDataList = - (inWhere === 'my-crew' - ? data?.pages.flatMap((page) => page?.content) - : data?.pages?.flatMap((page) => page?.content)) ?? []; + inWhere === 'my-crew' + ? data.pages.flatMap((page) => page.content as MyCrewList[]) + : data.pages.flatMap((page) => page.content as MainCrewList[]); + const gridColsStyle = inWhere === 'my-crew' ? '' : 'lg:grid-cols-2'; - if (data?.pages[0] === undefined) - // 초기 로딩시 데이터 없을때 - return ( -
- -
- ); + // 초기 로딩시 데이터 없을때 + if (!data.pages.length) return null; if (!crewDataList.length) return ( @@ -85,12 +69,10 @@ function CrewCardList( ))} - {isFetchingNextPage ? ( + {isFetchingNextPage && (
- ) : ( -
)} ); diff --git a/src/components/common/crew-list/crew-card.tsx b/src/components/common/crew-list/crew-card.tsx index 37552551..2daa2187 100644 --- a/src/components/common/crew-list/crew-card.tsx +++ b/src/components/common/crew-list/crew-card.tsx @@ -10,7 +10,7 @@ import IcoUser from '@/public/assets/icons/ic-user.svg'; import Profiles from './profiles'; interface CrewCardProps extends MainCrewList { - inWhere?: 'my-crew'; + inWhere?: 'my-crew' | 'main-crew'; } export default function CrewCard({ @@ -27,9 +27,7 @@ export default function CrewCard({ inWhere, }: CrewCardProps) { const [prefetched, setPrefetched] = useState(new Set()); - // NOTE: api연결 후 되돌리기 - // const CREWPAGE = `/crew/detail/${id}`; - const CREWPAGE = `/crew/detail/1`; + const CREWPAGE = `/crew/detail/${id}`; const router = useRouter(); const handleCardClick = () => { diff --git a/src/components/common/input/pop-over-calendar/index.tsx b/src/components/common/input/pop-over-calendar/index.tsx index 5f76e8c7..648314cf 100644 --- a/src/components/common/input/pop-over-calendar/index.tsx +++ b/src/components/common/input/pop-over-calendar/index.tsx @@ -36,9 +36,14 @@ export default function PopOverCalendar({ value, onChange }: PopOverProps) {