Skip to content

Commit

Permalink
Merge pull request #178 from INtiful/refactor/go/KAN-117-mypage
Browse files Browse the repository at this point in the history
refactor: mypage 리팩토링
  • Loading branch information
hakyoung12 authored Oct 14, 2024
2 parents 17a5a08 + bb7ed06 commit 5649456
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 53 deletions.
20 changes: 14 additions & 6 deletions src/app/(main)/mypage/_component/MyGatheringList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import Card from '@/app/components/Card/Card';
import InfiniteScroll from '@/app/components/InfiniteScroll/InfiniteScroll';
import ReviewModal from '@/app/components/Modal/ReviewModal';
import { useState } from 'react';
import { UserJoinedGatheringsData } from '@/types/data.type';
import useParticipation from '@/hooks/useParticipation';
import { UserData } from '@/types/client.type';

const MyGatheringList = () => {
interface MyGatheringListProps {
user: UserData | null;
initData: UserJoinedGatheringsData[];
}

const MyGatheringList = ({ initData, user }: MyGatheringListProps) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [cardId, setCardId] = useState<number>(0);
const { handleWithdrawClickWithId } = useParticipation(user);

const handleOpenModal = (id: number) => {
setCardId(id);
Expand All @@ -22,21 +31,20 @@ const MyGatheringList = () => {
return (
<>
<InfiniteScroll
initData={initData}
queryKey={['myGatherings']}
queryFn={getMyGathergins}
emptyText='아직 참여한 모임이 없습니다.'
errorText='모임을 불러오지 못했습니다.'
renderItem={(item, index) => (
<Card
handleSaveDiscard={() => console.log('Save Discard')}
data={item}
>
<Card data={item}>
<Card.Chips />
<Card.Info />
<Card.Button
handleButtonClick={() => {
item.isCompleted
? handleOpenModal(item.id)
: console.log('Cancel gathering');
: handleWithdrawClickWithId(item.id, ['myGatherings']);
}}
/>
</Card>
Expand Down
13 changes: 13 additions & 0 deletions src/app/(main)/mypage/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import Loader from '@/app/components/Loader/Loader';

const Loading = () => {
return (
<div className='flex min-h-360 items-center justify-center'>
<Loader />;
</div>
);
};

export default Loading;
8 changes: 6 additions & 2 deletions src/app/(main)/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Metadata } from 'next';
import MyGatheringList from './_component/MyGatheringList';
import getMyGatherings from '@/app/api/actions/gatherings/getMyGatherings';
import { getUserData } from '@/app/api/actions/mypage/getUserData';

export const metadata: Metadata = {
title: '나의 모임 | Soothe With Me',
description: 'Soothe With Me 나의 모임 페이지입니다.',
};

const Mygatherings = () => {
return <MyGatheringList />;
const Mygatherings = async () => {
const myGatherings = await getMyGatherings();
const user = await getUserData();
return <MyGatheringList initData={myGatherings} user={user} />;
};

export default Mygatherings;
34 changes: 34 additions & 0 deletions src/app/api/actions/gatherings/getMyGatherings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use server';

import { getCookie } from '@/actions/auth/cookie/cookie';
import { GatheringType } from '@/types/data.type';

const getMyGatherings = async () => {
const token = await getCookie('token');
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/gatherings/joined?limit=10&offset=0`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
},
);

if (!res.ok) {
throw new Error('모임을 불러오지 못했습니다.');
}

const data: GatheringType[] = await res.json();

return data;
} catch (error) {
throw new Error(
error instanceof Error ? error.message : '모임을 불러오지 못했습니다.',
);
}
};

export default getMyGatherings;
16 changes: 9 additions & 7 deletions src/app/api/gatherings/joined/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { getCookie } from '@/actions/auth/cookie/cookie';
import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common';
import {
DEFAULT_GATHERINGS_LIMIT,
DEFAULT_GATHERINGS_OFFSET,
} from '@/constants/common';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const offset = Number(searchParams.get('offset')) || DEFAULT_OFFSET;
const limit = Number(searchParams.get('limit')) || DEFAULT_LIMIT;
const offset =
Number(searchParams.get('offset')) || DEFAULT_GATHERINGS_OFFSET;
const limit = Number(searchParams.get('limit')) || DEFAULT_GATHERINGS_LIMIT;

const token = await getCookie('token');

Expand All @@ -22,10 +26,8 @@ export async function GET(req: Request) {

const data = await response.json();

const paginatedData = data.slice(offset, offset + limit);

// 전체 데이터 길이와 비교하여 다음 페이지가 있는지 결정
const hasNextPage = paginatedData.length === limit;
// 데이터 길이와 비교하여 다음 페이지가 있는지 결정
const hasNextPage = data.length === limit;

return NextResponse.json({
data: data, // API에서 반환하는 데이터
Expand Down
8 changes: 6 additions & 2 deletions src/app/api/gatherings/service/getMyGathergins.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'use client';

import {
DEFAULT_GATHERINGS_LIMIT,
DEFAULT_GATHERINGS_OFFSET,
} from '@/constants/common';
import { FetchGatheringsResponse } from '@/types/data.type';

const getMyGathergins = async (
offset = 0,
limit = 5,
offset = DEFAULT_GATHERINGS_OFFSET,
limit = DEFAULT_GATHERINGS_LIMIT,
): Promise<FetchGatheringsResponse> => {
const response = await fetch(
`/api/gatherings/joined?offset=${offset}&limit=${limit}`,
Expand Down
20 changes: 17 additions & 3 deletions src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import { render, screen } from '@testing-library/react';
import { render, screen, RenderOptions } from '@testing-library/react';
import { useRouter } from 'next/navigation';
import BottomFloatingBar from './BottomFloatingBar';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserData } from '@/types/client.type';
import { GatheringParticipantsType } from '@/types/data.type';
import { ReactNode } from 'react';
import '@testing-library/jest-dom';

// 유저데이터 모킹
Expand All @@ -24,6 +26,16 @@ jest.mock('next/navigation', () => ({
useParams: jest.fn(() => ({ id: '1' })),
}));

// QueryClient 인스턴스 생성
const queryClient = new QueryClient();

// QueryClientProvider로 감싸주는 헬퍼 함수 정의
const renderWithQueryClient = (ui: ReactNode, options?: RenderOptions) =>
render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
options,
);

describe('BottomFloatingBar 컴포넌트 테스트', () => {
const mockRouter = { push: jest.fn() };

Expand All @@ -33,7 +45,8 @@ describe('BottomFloatingBar 컴포넌트 테스트', () => {

// 참가자일 때 기본 렌더링 확인
it('renders correctly when the user is a participant', () => {
render(
renderWithQueryClient(
// 헬퍼 함수 사용
<BottomFloatingBar
user={mockUser}
createdBy={12}
Expand Down Expand Up @@ -63,7 +76,8 @@ describe('BottomFloatingBar 컴포넌트 테스트', () => {

// 주최자일 때 기본 렌더링 확인
it('renders correctly when the user is the organizer', () => {
render(
renderWithQueryClient(
// 헬퍼 함수 사용
<BottomFloatingBar
user={mockUser}
createdBy={1}
Expand Down
91 changes: 70 additions & 21 deletions src/app/components/InfiniteScroll/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { DEFAULT_LIMIT } from '@/constants/common';
import {
DEFAULT_GATHERINGS_LIMIT,
DEFAULT_GATHERINGS_OFFSET,
} from '@/constants/common';
import Loader from '../Loader/Loader';
interface ItemWithId {
id: number;
}
Expand All @@ -13,43 +17,66 @@ interface InfiniteQueryResponse<T extends ItemWithId> {
}

interface InfiniteScrollProps<T extends ItemWithId> {
initData: T[];
queryKey: string[];
queryFn: (offset?: number) => Promise<InfiniteQueryResponse<T>>;
limit?: number;
emptyText: string;
errorText: string;
renderItem: (item: T, index: number) => JSX.Element;
}

const InfiniteScroll = <T extends ItemWithId>({
initData,
queryKey,
queryFn,
limit = DEFAULT_LIMIT,
limit = DEFAULT_GATHERINGS_LIMIT,
emptyText,
errorText,
renderItem,
}: InfiniteScrollProps<T>) => {
const { ref, inView } = useInView({
triggerOnce: false,
threshold: 1.0,
const [topGradientVisible, setTopGradientVisible] = useState(false);
const [bottomGradientVisible, setBottomGradientVisible] = useState(false);

const { ref, inView } = useInView({ threshold: 0 });
const { ref: firstGatheringRef, inView: firstInView } = useInView({
threshold: 0,
});
const { ref: lastGatheringRef, inView: lastInView } = useInView({
threshold: 0,
});

const { data, isError, isFetching, fetchNextPage, hasNextPage } =
useInfiniteQuery({
queryKey,
queryFn: async ({ pageParam = DEFAULT_LIMIT }) => queryFn(pageParam),
queryFn: async ({ pageParam = DEFAULT_GATHERINGS_LIMIT }) =>
queryFn(pageParam),
getNextPageParam: (lastPage) => {
return lastPage.hasNextPage ? lastPage.offset + limit : undefined;
},
initialPageParam: 0,
initialPageParam: DEFAULT_GATHERINGS_OFFSET,
initialData: {
pages: [
{
hasNextPage: initData.length === DEFAULT_GATHERINGS_LIMIT,
offset: DEFAULT_GATHERINGS_OFFSET,
data: initData,
},
],
pageParams: [DEFAULT_GATHERINGS_OFFSET],
},
});

useEffect(() => {
setTopGradientVisible(!firstInView);
setBottomGradientVisible(!lastInView);
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView]);
}, [inView, firstInView, lastInView]);

// 데이터가 없거나 비어있을 때 emptyText를 표시
if (!data || data.pages.length === 0) {
if (!data || data.pages[0].data.length === 0) {
return (
<div className='flex grow items-center justify-center text-[14px] font-medium text-gray-500 dark:text-neutral-200'>
{emptyText}
Expand All @@ -60,25 +87,47 @@ const InfiniteScroll = <T extends ItemWithId>({
if (isError)
return (
<div className='flex grow items-center justify-center text-[14px] font-medium text-gray-500 dark:text-neutral-200'>
모임을 불러오지 못했습니다.
{errorText}
</div>
);

const allItems = data.pages.flatMap((page) => page.data);
return (
<>
{/* Top gradient */}
<div
className={`fixed left-0 right-0 top-56 z-[30] h-16 bg-gradient-to-b from-white to-transparent p-10 transition-opacity duration-500 ease-in-out md:top-60 ${
topGradientVisible ? 'opacity-100' : 'opacity-0'
}`}
/>

{/* Bottom gradient */}
<div
className={`fixed bottom-0 left-0 right-0 z-[30] h-16 bg-gradient-to-t from-white to-transparent p-10 transition-opacity duration-500 ease-in-out ${
bottomGradientVisible ? 'opacity-100' : 'opacity-0'
}`}
/>

<ul className='flex h-full flex-col'>
{data &&
data.pages.map((page) =>
page.data.map((item, index: number) => (
<li key={item.id}>{renderItem(item, index)}</li> // 사용자 정의 컴포넌트를 렌더링
)),
)}
{allItems.map((item, index) => (
<li
key={item.id}
ref={
index === 0
? firstGatheringRef
: index === initData.length - 1
? lastGatheringRef
: null
}
>
{renderItem(item, index)}
</li>
))}
</ul>
{isFetching && (
<div className='flex grow items-center justify-center'>
<div className='h-6 w-6 animate-spin rounded-full border-4 border-gray-300 border-t-transparent'></div>
<span className='ml-2 text-[14px] font-medium text-gray-500'>
Loading...
<div className='flex h-80 grow items-center justify-center'>
<span>
<Loader />
</span>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions src/app/components/Modal/ReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const ReviewModal = ({ gatheringId, onClose }: ReviewModalProps) => {
name='리뷰 등록'
type='button'
variant={score !== 0 && comment ? 'default' : 'gray'}
disabled={score === 0 || comment.length === 0}
onClick={handleSubmit}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export default async function RootLayout({
}: Readonly<{
children: ReactNode;
}>) {
const userData = await getUserData();
const user = await getUserData();
const token = await getCookie('token');
return (
<html lang='ko'>
<body className='flex min-h-dvh flex-col bg-var-gray-100 font-pretendard text-var-gray-900 dark:bg-neutral-950 dark:text-neutral-50'>
<Providers>
<Gnb user={userData} token={token} />
<Gnb user={user} token={token} />
<div className='grow pt-60'>{children}</div>
<div id='modal-root'></div>
<Toaster toastOptions={toastOptions} />
Expand Down
4 changes: 2 additions & 2 deletions src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const REVIEWS_PER_PAGE = 4;
export const MIN_PARTICIPANTS = 5;

// 무한스크롤 관련 데이터 패칭 상수
export const DEFAULT_OFFSET = 0;
export const DEFAULT_LIMIT = 5;
export const DEFAULT_GATHERINGS_OFFSET = 0;
export const DEFAULT_GATHERINGS_LIMIT = 10;

export const LIMIT_PER_REQUEST = 10;

Expand Down
Loading

0 comments on commit 5649456

Please sign in to comment.