diff --git a/src/app/meeting/[category]/page.tsx b/src/app/meeting/[category]/page.tsx index 84ec0e8..3cacd73 100644 --- a/src/app/meeting/[category]/page.tsx +++ b/src/app/meeting/[category]/page.tsx @@ -1,28 +1,71 @@ import FloatingButtonGroup from '@/components/common/FloatingButtonGroup'; +import { MEETING_QUERY_KEYS } from '@/hooks/queries/useMeetingQueries'; +import { translateCategoryNameToKor } from '@/util/searchFilter'; +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query'; +import { MEETING_TYPES } from 'constants/meeting-form/meetingConstants'; import { notFound } from 'next/navigation'; +import { getMeetings, getTopMeetings } from 'service/api/meeting'; +import { Paginated, SearchMeeting } from 'types/meeting'; import MeetingList from '../_features/MeetingList'; import RecommendMeeting from '../_features/RecommendMeeting'; -const ALLOWED_CATEGORIES = ['mogakco', 'study', 'side-project', 'hobby']; // 허용된 카테고리 리스트 - // 정적 경로 사전 생성 export async function generateStaticParams() { - return ALLOWED_CATEGORIES.map((category) => ({ category })); + return MEETING_TYPES.map((category) => category.id); } -function MeetingListPage({ params }: { params: { category: string } }) { +async function MeetingListPage({ params }: { params: { category: string } }) { const { category } = params; + const queryClient = new QueryClient(); + + const initialSearchQueryObj = { + keyword: '', + skillArray: [], + sortField: 'NEW', + lastMeetingId: 0, + size: 4, + }; + + await queryClient.prefetchQuery({ + queryKey: MEETING_QUERY_KEYS.topMeetings( + translateCategoryNameToKor(category), + ), + queryFn: () => getTopMeetings(translateCategoryNameToKor(category)), + }); + + await queryClient.prefetchInfiniteQuery({ + queryKey: MEETING_QUERY_KEYS.meetings( + translateCategoryNameToKor(category), + initialSearchQueryObj, + ), + queryFn: () => + getMeetings( + 0, + translateCategoryNameToKor(category), + initialSearchQueryObj, + ), + getNextPageParam: (lastPage: Paginated) => + lastPage.nextCursor ?? false, + initialPageParam: 0, + }); + // 허용되지 않은 category가 들어오면 404 페이지로 이동 - if (!ALLOWED_CATEGORIES.includes(category)) { + if (!MEETING_TYPES.map((category) => category.id).includes(category)) { notFound(); } return (
- - + + + +
); } diff --git a/src/app/meeting/_features/MeetingList.tsx b/src/app/meeting/_features/MeetingList.tsx index a1f09e5..d72747b 100644 --- a/src/app/meeting/_features/MeetingList.tsx +++ b/src/app/meeting/_features/MeetingList.tsx @@ -33,11 +33,11 @@ const MeetingList = () => { const { data, - isLoading, isError, hasNextPage, isFetchingNextPage, fetchNextPage, + isLoading, } = useInfiniteSearchMeetings( translateCategoryNameToKor(categoryStr), searchQuery, diff --git a/src/app/meeting/_features/skeleton/MeetingListSkeleton.tsx b/src/app/meeting/_features/skeleton/MeetingListSkeleton.tsx index 51a7810..de3e12b 100644 --- a/src/app/meeting/_features/skeleton/MeetingListSkeleton.tsx +++ b/src/app/meeting/_features/skeleton/MeetingListSkeleton.tsx @@ -4,50 +4,62 @@ const MeetingListSkeleton = () => { const skeletonCount = [1, 2, 3, 4]; return ( -
- - -
+
+
+ + + {/* 기술스택 */} + +
+ +
+ +
+
{skeletonCount.map((_, idx) => (
- -
+ +
- +
))}
+ {/* 테블릿 */}
{skeletonCount.map((_, idx) => (
- -
+ +
- +
))}
-
+
{skeletonCount.map((_, idx) => ( -
- -
- - +
+ +
+ +
diff --git a/src/components/ui/HorizonCard.tsx b/src/components/ui/HorizonCard.tsx index 6e964b9..15453d0 100644 --- a/src/components/ui/HorizonCard.tsx +++ b/src/components/ui/HorizonCard.tsx @@ -127,6 +127,7 @@ const HorizonCard = ({ }; const [thumbnail, setThumbnail] = useState(thumbnailUrl); + const [thumbnailLoaded, setThumbnailLoaded] = useState(false); return (
+ {!thumbnailLoaded && ( +
+ )} + {!thumbnailLoaded && ( +
+ )} card_thumbnail setThumbnail('/thumbnail.jpg')} + onLoad={() => setThumbnailLoaded(true)} />
-
+
{title} - +
+ +
{likesCount}
+
-
+
- {location} diff --git a/src/constants/meeting-form/meetingConstants.tsx b/src/constants/meeting-form/meetingConstants.tsx index d87ce55..e021bc7 100644 --- a/src/constants/meeting-form/meetingConstants.tsx +++ b/src/constants/meeting-form/meetingConstants.tsx @@ -1,3 +1,34 @@ +import { BookOpen, Code, CodeXml, Palette } from 'lucide-react'; +import React from 'react'; + +// 모임 유형 옵션 +export const MEETING_TYPES = [ + { + id: 'mogakco', + label: '모각코', + icon: , + href: '/meeting/mogakco', + }, + { + id: 'study', + label: '스터디', + icon: , + href: '/meeting/study', + }, + { + id: 'side-project', + label: '사이드 프로젝트', + icon: , + href: '/meeting/side-project', + }, + { + id: 'hobby', + label: '취미', + icon: , + href: '/meeting/hobby', + }, +]; + // 가입 방식 옵션 export const JOIN_METHODS = [ { id: 'immediate', label: '바로 참가' }, diff --git a/src/service/api/meeting.ts b/src/service/api/meeting.ts index 34cff72..157cc15 100644 --- a/src/service/api/meeting.ts +++ b/src/service/api/meeting.ts @@ -14,9 +14,12 @@ const getTopMeetings = async ( ): Promise => { const token = await getAccessToken(); - const res = await (token ? authAPI : basicAPI).get(`/api/v1/meetings/top`, { - params: { categoryTitle }, - }); + const res = await (token ? authAPI : basicAPI).get( + `${process.env.NEXT_PUBLIC_API_URL}api/v1/meetings/top`, + { + params: { categoryTitle }, + }, + ); return res.data.data; }; @@ -30,7 +33,7 @@ const getMeetings = async ( const token = await getAccessToken(); const res = await (token ? authAPI : basicAPI).post( - `/api/v1/meetings/search?categoryTitle=${category}`, + `${process.env.NEXT_PUBLIC_API_URL}api/v1/meetings/search?categoryTitle=${category}`, newSearchQueryObj, );