Skip to content
57 changes: 50 additions & 7 deletions src/app/meeting/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchMeeting>) =>
lastPage.nextCursor ?? false,
initialPageParam: 0,
});

// ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ category๊ฐ€ ๋“ค์–ด์˜ค๋ฉด 404 ํŽ˜์ด์ง€๋กœ ์ด๋™
if (!ALLOWED_CATEGORIES.includes(category)) {
if (!MEETING_TYPES.map((category) => category.id).includes(category)) {
notFound();
}
return (
<div className="mb-[130px] mt-[88px]">
<FloatingButtonGroup />
<RecommendMeeting />
<MeetingList />
<HydrationBoundary state={dehydrate(queryClient)}>
<RecommendMeeting />
<MeetingList />
</HydrationBoundary>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/meeting/_features/MeetingList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ const MeetingList = () => {

const {
data,
isLoading,
isError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
isLoading,
} = useInfiniteSearchMeetings(
translateCategoryNameToKor(categoryStr),
searchQuery,
Expand Down
44 changes: 28 additions & 16 deletions src/app/meeting/_features/skeleton/MeetingListSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,62 @@ const MeetingListSkeleton = () => {
const skeletonCount = [1, 2, 3, 4];

return (
<div className="relative p-4">
<SkeletonBasic className="h-[47px] w-full" />
<SkeletonBasic className="mt-3 h-[40px] w-full md:absolute md:right-4 md:h-[47px] md:w-[122px]" />
<div className="mt-20 hidden flex-col md:hidden lg:flex">
<div className="relative mt-[126px] px-4">
<div className="flex flex-col items-center">
<SkeletonBasic className="mb-10 h-[47px] w-full sm:w-11/12" />
<SkeletonBasic className="h-[47px] w-full sm:w-11/12" />
{/* ๊ธฐ์ˆ ์Šคํƒ */}
<SkeletonBasic className="mt-8 h-[250px] sm:w-11/12 md:right-4 md:w-full" />
</div>

<div className="mx-8 mt-6 flex h-[40px] w-11/12 items-center">
<SkeletonBasic className=" w-full md:absolute md:right-3 md:h-[47px] md:w-[122px]" />
</div>
<div className="mt-5 hidden flex-col md:hidden lg:flex">
{skeletonCount.map((_, idx) => (
<div key={idx} className="flex h-auto w-full bg-BG py-4">
<SkeletonBasic className="h-[252px] w-[303px] p-4" />
<div className="ml-2 h-full flex-1 px-[40px]">
<SkeletonBasic className="h-[208px] w-[252px] p-4" />
<div className="ml-2 flex w-[738px] flex-col px-[30px] py-6">
<SkeletonBasic className="mt-2 h-[50px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[16px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[10px] w-auto flex-1" />
</div>
<div className="relative ml-2 h-full flex-1">
<SkeletonBasic className="absolute right-0 h-[30px] w-[30px] flex-1" />
<SkeletonBasic className="mt-12 h-[120px] w-auto flex-1" />
<SkeletonBasic className="mt-12 h-[60px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[46px] w-auto flex-1" />
</div>
</div>
))}
</div>
{/* ํ…Œ๋ธ”๋ฆฟ */}
<div className="mt-20 hidden flex-col md:flex lg:hidden">
{skeletonCount.map((_, idx) => (
<div key={idx} className="flex h-auto w-full bg-BG py-4">
<SkeletonBasic className="h-[160px] w-[160px] p-4" />
<div className="ml-2 h-full flex-1 px-[20px]">
<SkeletonBasic className="h-[208px] w-[252px] p-4" />
<div className="ml-2 flex w-full flex-1 flex-col px-[20px] py-6">
<SkeletonBasic className="mt-2 h-[50px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[16px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[10px] w-auto flex-1" />
</div>
<div className="relative ml-2 h-full w-[180px]">
<SkeletonBasic className="absolute right-0 h-[30px] w-[30px] flex-1" />
<SkeletonBasic className="mt-10 h-[60px] w-auto flex-1" />
<SkeletonBasic className="mt-10 h-[120px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[40px] w-auto flex-1" />
</div>
</div>
))}
</div>
<div className="mt-20 flex flex-col md:hidden lg:hidden">
<div className="flex flex-col sm:mt-5 md:mt-20 md:hidden lg:hidden">
{skeletonCount.map((_, idx) => (
<div key={idx} className="h-auto w-full flex-col bg-BG py-4">
<SkeletonBasic className="h-[160px] w-full" />
<div className="h-full flex-1">
<SkeletonBasic className="mt-2 h-[30px] w-auto flex-1" />
<SkeletonBasic className="mt-2 h-[16px] w-auto flex-1" />
<div
key={idx}
className="relative flex h-auto w-full flex-col items-center bg-BG py-4"
>
<SkeletonBasic className="h-[160px] w-[310px]" />
<div className="h-full w-[310px]">
<SkeletonBasic className="mt-3 h-[30px] w-auto flex-1" />
<SkeletonBasic className="mb-10 mt-4 h-[130px] w-auto flex-1" />
<SkeletonBasic className="mt-5 h-[40px] w-auto flex-1" />
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/components/ui/HorizonCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const HorizonCard = ({
};

const [thumbnail, setThumbnail] = useState(thumbnailUrl);
const [thumbnailLoaded, setThumbnailLoaded] = useState(false);

return (
<div
Expand Down Expand Up @@ -165,6 +166,9 @@ const HorizonCard = ({
className="relative flex-shrink-0"
style={{ height: `${thumbnailHeight}px`, width: `${thumbnailWidth}px` }}
>
{!thumbnailLoaded && (
<div className="h-full w-full animate-pulse rounded-[20px] bg-Cgray200"></div>
)}
<Image
className="rounded-[20px] object-cover"
src={thumbnail ? thumbnail : '/thumbnail.jpg'}
Expand Down
37 changes: 22 additions & 15 deletions src/components/ui/VerticalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const VerticalCard = ({
};

const [thumbnail, setThumbnail] = useState(thumbnailUrl);
const [thumbnailLoaded, setThumbnailLoaded] = useState(false);

return (
<div
Expand All @@ -146,35 +147,41 @@ const VerticalCard = ({
className={'relative'}
style={{ height: `${thumbnailHeight}px`, width: `${thumbnailWidth}px` }}
>
{!thumbnailLoaded && (
<div className="h-full w-full animate-pulse rounded-[20px] bg-Cgray200"></div>
)}
<Image
className="rounded-[20px] object-cover"
src={thumbnail ? thumbnail : '/thumbnail.jpg'}
alt="card_thumbnail"
fill
onError={() => setThumbnail('/thumbnail.jpg')}
onLoad={() => setThumbnailLoaded(true)}
/>
</div>
<div className="mt-4">
<div className="typo-head2 flex justify-between truncate text-Cgray800 ">
<div className="typo-head2 relative flex h-[40px] justify-between truncate text-Cgray800 ">
<span className="max-w-[270px] overflow-hidden truncate text-ellipsis whitespace-nowrap">
{title}
</span>
<Button
className="relative h-auto w-auto"
variant="text"
size="sm"
onClick={(e) => handleLikeButton(e)}
icon={
<Heart
style={{ width: '24px', height: '24px' }}
className={isLike ? 'fill-main' : 'stroke-Cgray500'}
/>
}
></Button>
<div className="flex flex-col items-center">
<Button
className="right-0 h-auto w-auto"
variant="text"
size="sm"
onClick={(e) => handleLikeButton(e)}
icon={
<Heart
style={{ width: '24px', height: '24px' }}
className={isLike ? 'fill-main' : 'stroke-Cgray500'}
/>
}
></Button>
<div className="typo-caption2 text-Cgray500">{likesCount}</div>
</div>
</div>
<div className="mt-3 flex items-center gap-1 truncate text-Cgray500">
<div className="relative flex items-center gap-1 truncate text-Cgray500">
<Map size={20} strokeWidth={1} />

<span className="max-w-[270px] overflow-hidden truncate text-ellipsis whitespace-nowrap">
{location}
</span>
Expand Down
31 changes: 31 additions & 0 deletions src/constants/meeting-form/meetingConstants.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
import { BookOpen, Code, CodeXml, Palette } from 'lucide-react';
import React from 'react';

// ๋ชจ์ž„ ์œ ํ˜• ์˜ต์…˜
export const MEETING_TYPES = [
{
id: 'mogakco',
label: '๋ชจ๊ฐ์ฝ”',
icon: <Code className="h-5 w-5" />,
href: '/meeting/mogakco',
},
{
id: 'study',
label: '์Šคํ„ฐ๋””',
icon: <BookOpen className="h-5 w-5" />,
href: '/meeting/study',
},
{
id: 'side-project',
label: '์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ',
icon: <CodeXml className="h-5 w-5" />,
href: '/meeting/side-project',
},
{
id: 'hobby',
label: '์ทจ๋ฏธ',
icon: <Palette className="h-5 w-5" />,
href: '/meeting/hobby',
},
];

// ๊ฐ€์ž… ๋ฐฉ์‹ ์˜ต์…˜
export const JOIN_METHODS = [
{ id: 'immediate', label: '๋ฐ”๋กœ ์ฐธ๊ฐ€' },
Expand Down
11 changes: 7 additions & 4 deletions src/service/api/meeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ const getTopMeetings = async (
): Promise<TopMeeting[]> => {
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;
};
Expand All @@ -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,
);

Expand Down
Loading