diff --git a/package-lock.json b/package-lock.json index 372bd8bc..7affbb9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@storybook/nextjs": "^8.3.5", "@storybook/react": "^8.3.5", "@storybook/testing-library": "^0.2.2", + "@tanstack/react-query-devtools": "^5.59.20", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20", "@types/react": "^18", @@ -4200,6 +4201,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.59.20.tgz", + "integrity": "sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.59.20", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.20.tgz", @@ -4215,6 +4226,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.20.tgz", + "integrity": "sha512-AL/eQS1NFZhwwzq2Bq9Gd8wTTH+XhPNOJlDFpzPMu9NC5CQVgA0J8lWrte/sXpdWNo5KA4hgHnEdImZsF4h6Lw==", + "dev": true, + "dependencies": { + "@tanstack/query-devtools": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.59.20", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-query-next-experimental": { "version": "5.59.20", "resolved": "https://registry.npmjs.org/@tanstack/react-query-next-experimental/-/react-query-next-experimental-5.59.20.tgz", diff --git a/package.json b/package.json index ea50637a..565c46af 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@storybook/nextjs": "^8.3.5", "@storybook/react": "^8.3.5", "@storybook/testing-library": "^0.2.2", + "@tanstack/react-query-devtools": "^5.59.20", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20", "@types/react": "^18", diff --git a/src/_apis/crew/get-crew-list.ts b/src/_apis/crew/get-crew-list.ts index 15b132c7..75a561ac 100644 --- a/src/_apis/crew/get-crew-list.ts +++ b/src/_apis/crew/get-crew-list.ts @@ -1,25 +1,25 @@ import { fetchApi } from '@/src/utils/api'; -import { MainCrewList, MainCrewListResponse } from '@/src/types/crew-card'; +import { ConditionTypes, MainCrewListResponse, PageableTypes } from '@/src/types/crew-card'; + +export async function getCrewList(condition: ConditionTypes, pageable: PageableTypes) { + const { keyword, mainLocation, mainCategory, subCategory, sortType } = condition; + const { page, size, sort = ['string'] } = pageable; -export async function getCrewList(page: number, limit: number): Promise { try { const response = await fetchApi( - `/crews?_page=${page + 1}&_limit=${limit}`, + `/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', // 인증 정보를 요청에 포함 }, ); - if (!Array.isArray(response)) { - throw new Error('서버 응답이 올바른 형식이 아닙니다.'); - } - const data = response as MainCrewList[]; - const hasNext = data.length === limit; - - return { data, hasNext }; + return response; } catch (error) { - throw new Error('크루 리스트를 불러오는데 실패했습니다.'); + // eslint-disable-next-line no-console + console.error(error); + return undefined; } } diff --git a/src/_queries/crew-queries.tsx b/src/_queries/crew-queries.tsx index f6386fd4..d9c0a926 100644 --- a/src/_queries/crew-queries.tsx +++ b/src/_queries/crew-queries.tsx @@ -1,20 +1,26 @@ -import { MainCrewList } from '@/src/types/crew-card'; +import { UseInfiniteQueryOptions } from '@tanstack/react-query'; +import { ConditionTypes, MainCrewListResponse, PageableTypes } from '@/src/types/crew-card'; import { getCrewList } from '../_apis/crew/get-crew-list'; -export function useGetCrewQuery() { - interface QueryParams { - pageParam?: number; - } - - interface Page { - hasNext: boolean; - } - +export function useGetCrewListQuery(condition: ConditionTypes) { return { - queryKey: ['crew'], - queryFn: ({ pageParam = 0 }: QueryParams) => getCrewList(pageParam, 3), - getNextPageParam: (lastPage: Page, allPages: Page[]) => - lastPage.hasNext ? allPages.length + 1 : undefined, - select: (data: MainCrewList[]) => data, // 그대로 반환 + queryKey: [ + condition.keyword, + condition.mainLocation, + condition.mainCategory, + condition.subCategory, + 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; + }, + ), + getNextPageParam: (lastPage: MainCrewListResponse, allPages: MainCrewListResponse[]) => + lastPage.hasNext ? allPages.length : undefined, }; } diff --git a/src/app/(crew)/my-crew/page.tsx b/src/app/(crew)/my-crew/page.tsx index facac53b..2dcff0ac 100644 --- a/src/app/(crew)/my-crew/page.tsx +++ b/src/app/(crew)/my-crew/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { useGetCrewQuery } from '@/src/_queries/crew-queries'; +import { useGetCrewListQuery } from '@/src/_queries/crew-queries'; import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; import Tabs from '@/src/components/common/tab'; @@ -17,7 +17,7 @@ export default function MyCrewPage() { // TODO: fetchCrewData 함수를 사용하여 데이터를 불러오기 : 파라미터 수정 필요 // TODO: 리스트와는 다른 데이터를 사용해야해서 우선 주석처리 했습니다. // const { data, ref, isFetchingNextPage } = - // useInfiniteScroll(useGetCrewQuery()); + // useInfiniteScroll(useGetCrewListQuery()); return (
diff --git a/src/app/(crew)/page.tsx b/src/app/(crew)/page.tsx index db3891ea..f819f5b1 100644 --- a/src/app/(crew)/page.tsx +++ b/src/app/(crew)/page.tsx @@ -1,83 +1,26 @@ -'use client'; +import { getCrewList } from '@/src/_apis/crew/get-crew-list'; +import FindCrew from '../_components/find-crew/find-crew'; -import { useState } from 'react'; -import Image from 'next/image'; -import { Divider } from '@mantine/core'; -import { useGetCrewQuery } from '@/src/_queries/crew-queries'; -import regionData from '@/src/data/region.json'; -import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; -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 TextInput from '@/src/components/common/input/text-input'; -import { MainCrewListResponse } from '@/src/types/crew-card'; -import IcoSearch from '@/public/assets/icons/ic-search.svg'; - -export default function Home() { - const [mainCategory, setMainCategory] = useState('cardio_strength'); - const [subCategory, setSubCategory] = useState('running'); - const [sort, setSort] = useState('latest'); - const [region, setRegion] = useState('all'); - const [search, setSearch] = useState(''); +export default async function HomePage() { + const initialData = await getCrewList( + { + keyword: '', + mainLocation: '', + mainCategory: '', + subCategory: '', + sortType: 'LATEST', + }, + { + page: 0, + size: 6, + sort: ['LATEST'], + }, + ); - const { data, ref, isFetchingNextPage } = - useInfiniteScroll(useGetCrewQuery()); + const infiniteData = { + pages: [initialData], + pageParams: [], + }; - return ( -
-
- - -
- -
-
-
- setSearch(e.target.value)} - leftSectionPointerEvents="none" - leftSection={ - search - } - placeholder="크루 이름, 위치를 검색하세요." - inputClassNames="w-full h-11 pl-12 placeholder:text-gray-500 font-pretendard text-base font-medium text-gray-800 rounded-xl" - /> -
-
- dataItem.main)} - placeholder="전체" - value={region} - className="w-[130px]" - onChange={setRegion} - /> - -
-
-
-
- -
-
- ); + return ; } diff --git a/src/app/_components/category/category-container/index.tsx b/src/app/_components/category/category-container/index.tsx index 5c31d3f9..67c1ed3d 100644 --- a/src/app/_components/category/category-container/index.tsx +++ b/src/app/_components/category/category-container/index.tsx @@ -1,6 +1,4 @@ -'use client'; - -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import category from '@/src/data/category.json'; import InternalCategory from '@/src/app/_components/category/internal-category'; import MainCategory from '@/src/app/_components/category/main-category'; @@ -18,32 +16,26 @@ export default function CategoryContainer({ setMainCategory, setSubCategory, }: CategoryContainerProps) { - const [categoryIndex, setCategoryIndex] = useState(0); - - useEffect(() => { - if (subCategory !== category[categoryIndex].items[0].value) { - setSubCategory(category[categoryIndex].items[0].value); - } - }, [mainCategory, categoryIndex]); - - useEffect(() => { - if (mainCategory !== category[categoryIndex].title.value) { - setMainCategory(category[categoryIndex].title.value); - } - }, [subCategory]); + const [categoryIndex, setCategoryIndex] = useState(null); return (
{ + setMainCategory(newValue); + setSubCategory(''); + }} onHover={setCategoryIndex} /> { + setSubCategory(newValue); + setMainCategory(category[categoryIndex ?? 0].title.label); + }} />
); diff --git a/src/app/_components/category/internal-category/index.tsx b/src/app/_components/category/internal-category/index.tsx index 994e0b5e..754ebd0e 100644 --- a/src/app/_components/category/internal-category/index.tsx +++ b/src/app/_components/category/internal-category/index.tsx @@ -30,8 +30,8 @@ export default function InternalCategory({ value, category, onChange }: Internal
  • diff --git a/src/app/_components/category/main-category/index.tsx b/src/app/_components/category/main-category/index.tsx index 50063dad..2caac939 100644 --- a/src/app/_components/category/main-category/index.tsx +++ b/src/app/_components/category/main-category/index.tsx @@ -62,8 +62,8 @@ export default function MainCategory({ value, category, onHover, onChange }: Mai > + } + placeholder="크루 이름, 위치를 검색하세요." + classNames={{ + input: + 'h-11 w-full rounded-xl border-0 pr-10 font-pretendard text-base font-medium text-gray-800 placeholder:text-gray-500', + }} + /> +
  • +
    + dataItem.main)} + placeholder="지역 전체" + value={region} + className="w-[130px]" + onChange={(newValue) => { + setRegion(newValue as string); + }} + /> + { + setSort(newValue as string); + }} + /> +
    + + +
    + +
    + + ); +} diff --git a/src/components/client-provider.tsx b/src/components/client-provider.tsx index 3528116a..91a397cc 100644 --- a/src/components/client-provider.tsx +++ b/src/components/client-provider.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'; export default function ClientProvider({ children }: { children: ReactNode }) { @@ -16,6 +17,7 @@ export default function ClientProvider({ children }: { children: ReactNode }) { return ( + {process.env.NODE_ENV === 'development' && } {children} ); 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 c6533340..94e77ad9 100644 --- a/src/components/common/crew-list/crew-card-list.stories.tsx +++ b/src/components/common/crew-list/crew-card-list.stories.tsx @@ -1,5 +1,7 @@ +import { useEffect, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { useGetCrewQuery } from '@/src/_queries/crew-queries'; +import { InfiniteData } from '@tanstack/react-query'; +import { useGetCrewListQuery } from '@/src/_queries/crew-queries'; import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; import ClientProvider from '@/src/components/client-provider'; import { MainCrewListResponse } from '@/src/types/crew-card'; @@ -27,14 +29,36 @@ const meta: Meta = { export default meta; type Story = StoryObj; -function RenderCrewCardList() { - const { data, ref, isFetchingNextPage } = - useInfiniteScroll(useGetCrewQuery()); +function RenderCrewCardList({ + initialData, +}: { + initialData: InfiniteData; +}) { + const [data, setData] = useState>(initialData); + const { + data: CrewCardListData, + ref, + isFetchingNextPage, + } = useInfiniteScroll( + useGetCrewListQuery({ + keyword: '', + mainLocation: '', + mainCategory: '', + subCategory: '', + sortType: 'LATEST', + }), + ); + + useEffect(() => { + if (CrewCardListData) { + setData(CrewCardListData); + } + }, [CrewCardListData]); return ; } export const Default: Story = { - render: () => , + render: () => , args: {}, }; diff --git a/src/components/common/crew-list/crew-card-list.tsx b/src/components/common/crew-list/crew-card-list.tsx index a7435074..d182cdeb 100644 --- a/src/components/common/crew-list/crew-card-list.tsx +++ b/src/components/common/crew-list/crew-card-list.tsx @@ -11,13 +11,13 @@ import CrewCard from './crew-card'; // CrewCardListProps 타입을 구분하여 정의 interface MainCrewCardListProps { - data: InfiniteData | undefined; + data: InfiniteData; isFetchingNextPage: boolean; inWhere?: undefined; } interface MyCrewCardListProps { - data: InfiniteData | undefined; + data: InfiniteData; isFetchingNextPage: boolean; inWhere: 'my-crew'; } @@ -31,44 +31,52 @@ function CrewCardList( ) { const crewDataList = (inWhere === 'my-crew' - ? data?.pages.flatMap((page) => page.data as MyCrewList[]) - : data?.pages.flatMap((page) => page.data as MainCrewList[])) ?? []; + ? data?.pages.flatMap((page) => page?.data as MyCrewList[]) + : data?.pages?.flatMap((page) => page?.content as MainCrewList[])) ?? []; const gridColsStyle = inWhere === 'my-crew' ? '' : 'lg:grid-cols-2'; - if (!crewDataList.length) + if (data?.pages[0] === undefined) + // 초기 로딩시 데이터 없을때 return (
    ); + if (!crewDataList.length) + return ( +
    +

    데이터가 없습니다.

    +
    + ); + return ( <>
      {crewDataList.map((inform) => ( // NOTE: 데이터 이름 변경이 많은 곳이라 dev로 보면 아마 undefined로 나오지만(목데이터도 변경이 필요함..) // NOTE: 추후 백앤드 api를 fetch 하면 정상적으로 확인할 수 있습니다. -
    • +
    • ))} diff --git a/src/components/common/crew-list/crew-card.tsx b/src/components/common/crew-list/crew-card.tsx index 77e33ce4..37552551 100644 --- a/src/components/common/crew-list/crew-card.tsx +++ b/src/components/common/crew-list/crew-card.tsx @@ -4,22 +4,12 @@ import { useState } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import ProgressBar from '@/src/components/common/progress-bar/index'; -import { CrewMember } from '@/src/types/crew-card'; +import { MainCrewList } from '@/src/types/crew-card'; import Check from '@/public/assets/icons/ic-check.svg'; -import UserIco from '@/public/assets/icons/ic-user.svg'; +import IcoUser from '@/public/assets/icons/ic-user.svg'; import Profiles from './profiles'; -interface CrewCardProps { - id: number; - title: string; - mainLocation: string; - subLocation: string; - participantCount: number; - totalCount: number; - isConfirmed?: boolean; - imageUrl: string; - totalGatheringCount: number; - crewMembers?: CrewMember[]; +interface CrewCardProps extends MainCrewList { inWhere?: 'my-crew'; } @@ -82,7 +72,7 @@ export default function CrewCard({
      - user icon + 모집 인원 중 참여 인원 {participantCount}/{totalCount} @@ -94,7 +84,7 @@ export default function CrewCard({
      {isConfirmed && ( - 확인 + 개설 확정 )} diff --git a/src/components/common/input/text-input/index.tsx b/src/components/common/input/text-input/index.tsx index 0e740e81..205bbc4c 100644 --- a/src/components/common/input/text-input/index.tsx +++ b/src/components/common/input/text-input/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { RefObject } from 'react'; import { UseFormRegisterReturn } from 'react-hook-form'; import { TextInput as MantineTextInput, diff --git a/src/data/region.json b/src/data/region.json index ec7ad74b..b38ea22d 100644 --- a/src/data/region.json +++ b/src/data/region.json @@ -1,4 +1,8 @@ [ + { + "main": { "label": "지역 전체", "value": "" }, + "areas": [] + }, { "main": { "label": "서울특별시", "value": "seoul" }, "areas": [ diff --git a/src/hooks/use-infinite-scroll.ts b/src/hooks/use-infinite-scroll.ts index 94d84ce7..3a26ca17 100644 --- a/src/hooks/use-infinite-scroll.ts +++ b/src/hooks/use-infinite-scroll.ts @@ -10,7 +10,7 @@ export const useInfiniteScroll = ({ queryFn: ({ pageParam }: { pageParam?: number }) => Promise; getNextPageParam: (lastPage: TData, pages: TData[]) => number | undefined; }) => { - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useInfiniteQuery({ queryKey, queryFn, getNextPageParam, @@ -25,5 +25,5 @@ export const useInfiniteScroll = ({ fetchNextPage(); } - return { data, ref, isFetchingNextPage }; + return { data, ref, isFetchingNextPage, refetch }; }; diff --git a/src/types/crew-card.d.ts b/src/types/crew-card.d.ts index 153b3ad5..426826bb 100644 --- a/src/types/crew-card.d.ts +++ b/src/types/crew-card.d.ts @@ -1,12 +1,26 @@ -export interface MainCrewListResponse { - data: MainCrewList[]; - hasNext: boolean; +export interface ConditionTypes { + keyword: string; + mainLocation: string; + mainCategory: string; + subCategory: string; + sortType: 'LATEST' | 'POPULAR'; +} + +export interface PageableTypes { + page: number; + size: number; + sort: string[]; } +export type MainCrewListResponse = { + content: MainCrewList[] | undefined; + hasNext: boolean; +}; + export interface MainCrewList { id: number; - mainCategory: string; - subCategory: string; + mainCategory?: string; + subCategory?: string; title: string; mainLocation: string; subLocation: string; @@ -15,6 +29,7 @@ export interface MainCrewList { imageUrl: string; isConfirmed: boolean; totalGatheringCount: number; + crewMembers?: CrewMember[]; } export interface CrewMember {