diff --git a/src/app/(auth)/loading.tsx b/src/app/(auth)/loading.tsx index 50e35ca6..3bb70580 100644 --- a/src/app/(auth)/loading.tsx +++ b/src/app/(auth)/loading.tsx @@ -4,7 +4,7 @@ import Loader from '@/app/components/Loader/Loader'; const Loading = () => { return ( -
+
;
); diff --git a/src/app/(main)/gatherings/_component/ClientSideGatherings.tsx b/src/app/(main)/gatherings/_component/ClientSideGatherings.tsx index 8f7c731f..f068b34f 100644 --- a/src/app/(main)/gatherings/_component/ClientSideGatherings.tsx +++ b/src/app/(main)/gatherings/_component/ClientSideGatherings.tsx @@ -8,7 +8,7 @@ import MakeGatheringModal from '@/app/components/Modal/MakeGatheringModal'; import Tabs from '@/app/components/Tabs/Tabs'; import useGatherings from '@/hooks/useGatherings'; import usePreventScroll from '@/hooks/usePreventScroll'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import CreateGatheringButton from './CreateGatheringButton'; import Filters from '@/app/components/Filters/Filters'; import GatheringCardList from './GatheringCardList'; @@ -20,7 +20,7 @@ import { useInView } from 'react-intersection-observer'; import { SORT_OPTIONS } from '@/constants/common'; interface ClientSideGatheringsProps { - gatherings: GatheringsListData[]; + gatherings: GatheringType[]; user: UserData | null; } diff --git a/src/app/(main)/gatherings/_component/GatheringCardList.tsx b/src/app/(main)/gatherings/_component/GatheringCardList.tsx index 1fd62514..9546d383 100644 --- a/src/app/(main)/gatherings/_component/GatheringCardList.tsx +++ b/src/app/(main)/gatherings/_component/GatheringCardList.tsx @@ -2,13 +2,13 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import CardList from '@/app/components/CardList/CardList'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import { useSavedGatheringList } from '@/context/SavedGatheringContext'; import { useInView } from 'react-intersection-observer'; interface GatheringCardListProps { - gatherings: GatheringsListData[]; + gatherings: GatheringType[]; } const GatheringCardList = ({ gatherings }: GatheringCardListProps) => { diff --git a/src/app/(main)/gatherings/loading.tsx b/src/app/(main)/gatherings/loading.tsx index 50e35ca6..8a1d4420 100644 --- a/src/app/(main)/gatherings/loading.tsx +++ b/src/app/(main)/gatherings/loading.tsx @@ -5,7 +5,7 @@ import Loader from '@/app/components/Loader/Loader'; const Loading = () => { return (
- ; +
); }; diff --git a/src/app/(main)/mypage/_component/EmptyReviewPage.tsx b/src/app/(main)/mypage/_component/EmptyReviewPage.tsx new file mode 100644 index 00000000..3fc333bd --- /dev/null +++ b/src/app/(main)/mypage/_component/EmptyReviewPage.tsx @@ -0,0 +1,11 @@ +const EmptyReviewPage = () => { + return ( +
+

+ 아직 작성한 리뷰가 없어요. +

+
+ ); +}; + +export default EmptyReviewPage; diff --git a/src/app/(main)/mypage/_component/ReviewFilterButtons.tsx b/src/app/(main)/mypage/_component/ReviewFilterButtons.tsx deleted file mode 100644 index 9600dc70..00000000 --- a/src/app/(main)/mypage/_component/ReviewFilterButtons.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import Chip from '@/app/components/Chip/Chip'; - -interface ReviewFilterButtonsProps { - filterType: string; - setFilterType: (type: string) => void; -} - -const ReviewFilterButtons = ({ - filterType, - setFilterType, -}: ReviewFilterButtonsProps) => { - const filterTypeList = ['작성 가능한 리뷰', '작성한 리뷰'] as const; - - const handleChangeFilterType = (type: (typeof filterTypeList)[number]) => { - setFilterType(type); - }; - - return ( -
- {filterTypeList.map((type) => ( - - ))} -
- ); -}; - -export default ReviewFilterButtons; diff --git a/src/app/(main)/mypage/_component/ReviewFilterTab.tsx b/src/app/(main)/mypage/_component/ReviewFilterTab.tsx new file mode 100644 index 00000000..2477fc79 --- /dev/null +++ b/src/app/(main)/mypage/_component/ReviewFilterTab.tsx @@ -0,0 +1,50 @@ +'use client'; + +import Chip from '@/app/components/Chip/Chip'; +import { useRouter, usePathname } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +type filterType = 'writable' | 'written'; + +const ReviewFilterTab = () => { + const filterTypeButtons = [ + { name: '작성 가능한 리뷰', type: 'writable' }, + { name: '작성한 리뷰', type: 'written' }, + ]; + + const [currentFilterType, setCurrentFilterType] = useState( + null, + ); + + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + const path = pathname.replace('/mypage/review/', '') as filterType; + setCurrentFilterType(path); + }, [pathname]); + + const handleChangeFilterType = (filterType: filterType) => { + router.push(`/mypage/review/${filterType}`); + }; + + return ( +
+ {filterTypeButtons.map((filterType) => ( + + ))} +
+ ); +}; + +export default ReviewFilterTab; diff --git a/src/app/(main)/mypage/_component/Tab.tsx b/src/app/(main)/mypage/_component/Tab.tsx index 548469ee..423b76b4 100644 --- a/src/app/(main)/mypage/_component/Tab.tsx +++ b/src/app/(main)/mypage/_component/Tab.tsx @@ -6,15 +6,15 @@ import { usePathname } from 'next/navigation'; const tabList = [ { name: '나의 모임', - link: '/mypage', + link: ['/mypage'], }, { name: '나의 리뷰', - link: '/mypage/review', + link: ['/mypage/review/writable', '/mypage/review/written'], }, { name: '내가 만든 모임', - link: '/mypage/created', + link: ['/mypage/created'], }, ]; @@ -27,8 +27,11 @@ const Tab = () => { return ( diff --git a/src/app/(main)/mypage/created/_component/ClientSideGatherings.tsx b/src/app/(main)/mypage/created/_component/ClientSideGatherings.tsx index 2572e30f..dae247e1 100644 --- a/src/app/(main)/mypage/created/_component/ClientSideGatherings.tsx +++ b/src/app/(main)/mypage/created/_component/ClientSideGatherings.tsx @@ -5,11 +5,11 @@ import { useUserCreated } from '@/hooks/useUserCreated'; import GatheringList from './GatheringList'; import { useInView } from 'react-intersection-observer'; import { useEffect } from 'react'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import Loader from '@/app/components/Loader/Loader'; interface ClientSideGatheringsProps { - initialGatheringList: GatheringsListData[]; + initialGatheringList: GatheringType[]; createdBy: string; } diff --git a/src/app/(main)/mypage/created/_component/GatheringList.tsx b/src/app/(main)/mypage/created/_component/GatheringList.tsx index 7c857ff2..951a3ada 100644 --- a/src/app/(main)/mypage/created/_component/GatheringList.tsx +++ b/src/app/(main)/mypage/created/_component/GatheringList.tsx @@ -1,11 +1,11 @@ 'use client'; import Card from '@/app/components/Card/Card'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import Link from 'next/link'; interface GatheringListProps { - dataList: GatheringsListData[]; + dataList: GatheringType[]; } const GatheringList = ({ dataList }: GatheringListProps) => { diff --git a/src/app/(main)/mypage/review/page.tsx b/src/app/(main)/mypage/review/page.tsx deleted file mode 100644 index b9667eec..00000000 --- a/src/app/(main)/mypage/review/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { useUser } from '@/app/(auth)/context/UserContext'; -import getJoinedGatherings from '@/app/api/actions/gatherings/getJoinedGathering'; -import getReviewList from '@/app/api/actions/reviews/getReviewList'; -import Card from '@/app/components/Card/Card'; -import ReviewModal from '@/app/components/Modal/ReviewModal'; -import Review from '@/app/components/Review/Review'; -import { MYPAGE_REVIEW_TABS } from '@/constants/common'; -import usePreventScroll from '@/hooks/usePreventScroll'; -import { useQueries } from '@tanstack/react-query'; -import Link from 'next/link'; -import { useState } from 'react'; -import ReviewFilterButtons from '../_component/ReviewFilterButtons'; - -const Page = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [filterType, setFilterType] = useState( - MYPAGE_REVIEW_TABS.WRITABLE, - ); - const [cardId, setCardId] = useState(0); - - const { user } = useUser(); - - const results = useQueries({ - queries: [ - { - queryKey: ['writableReviews'], - queryFn: () => getJoinedGatherings(), - }, - { - queryKey: ['myreviews'], - queryFn: () => - getReviewList({ - userId: Number(user?.id), - sortBy: 'createdAt', - sortOrder: 'desc', - }), - }, - ], - }); - - const writableReviewData = results[0].data; - const reviewData = results[1].data; - - const filteredData = Array.isArray(writableReviewData) - ? writableReviewData.filter((data) => { - switch (filterType) { - case MYPAGE_REVIEW_TABS.WRITABLE: // 작성 가능한 리뷰 - return data?.isCompleted && !data?.isReviewed; - - case MYPAGE_REVIEW_TABS.WRITTEN: // 작성한 리뷰 - return data?.isCompleted && data?.isReviewed; - - default: - return true; - } - }) - : []; - - const handleOpenModal = (id: number) => { - setCardId(id); - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setIsModalOpen(false); - }; - - usePreventScroll(isModalOpen); - - return ( - <> -
- {/* chips */} - - {/* cards */} - {(() => { - switch (filterType) { - // 전체 혹은 작성 가능한 리뷰 - case MYPAGE_REVIEW_TABS.WRITABLE: - return filteredData?.length ? ( - filteredData.map((data) => ( - - - - - data.isCompleted && handleOpenModal(data.id) - } - /> - - )) - ) : ( - - ); - // 작성한 리뷰 - case MYPAGE_REVIEW_TABS.WRITTEN: - return reviewData?.length ? ( -
- {reviewData.map((review) => ( - - - - ))} -
- ) : ( - - ); - } - })()} -
- - {isModalOpen && ( - - )} - - ); -}; - -export default Page; - -const EmptyPage = () => { - return ( -
-

- 아직 작성한 리뷰가 없어요 -

-
- ); -}; diff --git a/src/app/(main)/mypage/review/writable/page.tsx b/src/app/(main)/mypage/review/writable/page.tsx new file mode 100644 index 00000000..edae1e8b --- /dev/null +++ b/src/app/(main)/mypage/review/writable/page.tsx @@ -0,0 +1,63 @@ +'use client'; + +import getJoinedGatherings from '@/app/api/actions/gatherings/getJoinedGathering'; +import Card from '@/app/components/Card/Card'; +import ReviewModal from '@/app/components/Modal/ReviewModal'; +import usePreventScroll from '@/hooks/usePreventScroll'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import EmptyReviewPage from '../../_component/EmptyReviewPage'; +import ReviewFilterTab from '../../_component/ReviewFilterTab'; + +const WritableReviewsPage = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [cardId, setCardId] = useState(0); + + const { data: writableReviews } = useQuery({ + queryKey: ['reviews', 'writable'], + queryFn: () => getJoinedGatherings({ completed: true, reviewed: false }), + }); + + const handleOpenModal = (id: number) => { + setCardId(id); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + usePreventScroll(isModalOpen); + + return ( + <> +
+ {/* tab */} + + + {/* cards */} + {writableReviews?.length ? ( + writableReviews.map((data) => ( + + + + + data.isCompleted && handleOpenModal(data.id) + } + /> + + )) + ) : ( + + )} +
+ + {isModalOpen && ( + + )} + + ); +}; + +export default WritableReviewsPage; diff --git a/src/app/(main)/mypage/review/written/page.tsx b/src/app/(main)/mypage/review/written/page.tsx new file mode 100644 index 00000000..414cd09d --- /dev/null +++ b/src/app/(main)/mypage/review/written/page.tsx @@ -0,0 +1,50 @@ +import { getUserData } from '@/app/api/actions/mypage/getUserData'; +import getReviewList from '@/app/api/actions/reviews/getReviewList'; +import Review from '@/app/components/Review/Review'; +import Link from 'next/link'; +import { redirect } from 'next/navigation'; +import EmptyReviewPage from '../../_component/EmptyReviewPage'; +import ReviewFilterTab from '../../_component/ReviewFilterTab'; + +const WrittenReviewsPage = async () => { + const user = await getUserData(); + + if (!user) { + redirect('/signin'); + } + + const writtenReviews = await getReviewList({ + userId: user?.id as number, + sortBy: 'createdAt', + sortOrder: 'desc', + }); + + return ( +
+ {/* tab */} + + + {/* cards */} + {writtenReviews?.length ? ( +
+ {writtenReviews.map((review) => ( + + + + ))} +
+ ) : ( + + )} +
+ ); +}; + +export default WrittenReviewsPage; diff --git a/src/app/(main)/saved/_component/SavedList.tsx b/src/app/(main)/saved/_component/SavedList.tsx index 8b8a91df..7abcffcf 100644 --- a/src/app/(main)/saved/_component/SavedList.tsx +++ b/src/app/(main)/saved/_component/SavedList.tsx @@ -2,11 +2,11 @@ import CardList from '@/app/components/CardList/CardList'; import { useSavedGatheringList } from '@/context/SavedGatheringContext'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import Link from 'next/link'; interface SavedListProps { - dataList: GatheringsListData[]; + dataList: GatheringType[]; } const SavedList = ({ dataList }: SavedListProps) => { diff --git a/src/app/api/actions/gatherings/getGatherings.ts b/src/app/api/actions/gatherings/getGatherings.ts index 45b721f1..0b54a23b 100644 --- a/src/app/api/actions/gatherings/getGatherings.ts +++ b/src/app/api/actions/gatherings/getGatherings.ts @@ -2,7 +2,7 @@ import qs from 'qs'; import { GatheringsType } from '@/types/client.type'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; interface GetGatheringsParams { id?: string; @@ -18,7 +18,7 @@ interface GetGatheringsParams { const getGatherings = async ( params: GetGatheringsParams = {}, -): Promise => { +): Promise => { try { const { limit = 10, offset = 0, ...rest } = params; @@ -48,7 +48,7 @@ const getGatherings = async ( throw new Error('모임을 불러오지 못했습니다.'); } - const data: GatheringsListData[] = await res.json(); + const data: GatheringType[] = await res.json(); return data; } catch (error) { diff --git a/src/app/api/actions/gatherings/getJoinedGathering.ts b/src/app/api/actions/gatherings/getJoinedGathering.ts index e05b7055..480376fa 100644 --- a/src/app/api/actions/gatherings/getJoinedGathering.ts +++ b/src/app/api/actions/gatherings/getJoinedGathering.ts @@ -18,8 +18,8 @@ const getJoinedGatherings = async ( ): Promise => { try { const { - completed = true, - reviewed = false, + completed, + reviewed, limit = 10, offset = 0, sortBy = 'dateTime', diff --git a/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx b/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx new file mode 100644 index 00000000..a3386b56 --- /dev/null +++ b/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { render, screen } from '@testing-library/react'; +import { useRouter } from 'next/navigation'; +import BottomFloatingBar from './BottomFloatingBar'; +import { UserData } from '@/types/client.type'; +import { GatheringParticipantsType } from '@/types/data.type'; +import '@testing-library/jest-dom'; + +// 유저데이터 모킹 +const mockUser: UserData = { + id: 1, + email: 'test@example.com', + name: 'Test User', + companyName: 'Test Company', + image: 'test-image.jpg', + createdAt: '2024-10-01T00:00:00Z', + updatedAt: '2024-10-03T00:00:00Z', +}; +const mockParticipantsData: GatheringParticipantsType[] = []; // 참가자 데이터 모킹 + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), + useParams: jest.fn(() => ({ id: '1' })), +})); + +describe('BottomFloatingBar 컴포넌트 테스트', () => { + const mockRouter = { push: jest.fn() }; + + beforeEach(() => { + (useRouter as jest.Mock).mockReturnValue(mockRouter); + }); + + // 참가자일 때 기본 렌더링 확인 + it('renders correctly when the user is a participant', () => { + render( + , + ); + + // 컴포넌트의 텍스트가 올바르게 렌더링되는지 확인 + expect( + screen.getByText('더 건강한 나와 팀을 위한 프로그램 🏃‍️️'), + ).toBeInTheDocument(); + expect( + screen.getByText( + '국내 최고 웰니스 전문가와 프로그램을 통해 지친 몸과 마음을 회복해봐요', + ), + ).toBeInTheDocument(); + + // 참여하기 버튼이 존재하는지 확인 + expect( + screen.getByRole('button', { name: '참여하기' }), + ).toBeInTheDocument(); + }); + + // 주최자일 때 기본 렌더링 확인 + it('renders correctly when the user is the organizer', () => { + render( + , + ); + + // 컴포넌트의 텍스트가 올바르게 렌더링되는지 확인 + expect( + screen.getByText('더 건강한 나와 팀을 위한 프로그램 🏃‍️️'), + ).toBeInTheDocument(); + expect( + screen.getByText( + '국내 최고 웰니스 전문가와 프로그램을 통해 지친 몸과 마음을 회복해봐요', + ), + ).toBeInTheDocument(); + + // 공유하기 버튼이 존재하는지 확인 + expect( + screen.getByRole('button', { name: '공유하기' }), + ).toBeInTheDocument(); + // 취소하기 버튼이 존재하는지 확인 + expect( + screen.getByRole('button', { name: '취소하기' }), + ).toBeInTheDocument(); + }); +}); diff --git a/src/app/components/BottomFloatingBar/Mock.ts b/src/app/components/BottomFloatingBar/Mock.ts deleted file mode 100644 index 007acc77..00000000 --- a/src/app/components/BottomFloatingBar/Mock.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* user mock data */ -export const userData = { - name: 'test name', - id: 1234, -}; - -/* 모임 상세 조회 mock data */ -export const groupData = { - id: 0, - registrationEnd: '2024-09-22T23:59:59Z', - participantCount: 5, - capacity: 10, - createdBy: 1232, - canceledAt: null, -}; - -/* 참가자 mock data */ -export const participantsData = [ - { - User: { - id: 1232, - }, - }, - { - User: { - id: 1534, - }, - }, - { - User: { - id: 1344, - }, - }, - { - User: { - id: 1654, - }, - }, - { - User: { - id: 1634, - }, - }, -]; - -/* 예비로 추가한 이벤트핸들링함수 */ -export const onCancel = () => console.log('모임이 취소되었습니다.'); -export const onShare = () => console.log('모임을 공유했습니다.'); -export const onJoin = () => console.log('모임에 참여했습니다.'); -export const onWithdraw = () => console.log('참여를 취소했습니다.'); diff --git a/src/app/components/BottomFloatingBar/ParticipationButton.test.tsx b/src/app/components/BottomFloatingBar/ParticipationButton.test.tsx new file mode 100644 index 00000000..309a2a66 --- /dev/null +++ b/src/app/components/BottomFloatingBar/ParticipationButton.test.tsx @@ -0,0 +1,310 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import ParticipationButton from './ParticipationButton'; +import { useRouter } from 'next/navigation'; +import useCopyUrlToClipboard from '@/hooks/useCopyUrlToClipboard'; +import useCancelGathering from '@/hooks/useCancelGathering'; +import useParticipation from '@/hooks/useParticipation'; +import { UserData } from '@/types/client.type'; +import Button from '../Button/Button'; +import '@testing-library/jest-dom'; + +// 유저데이터 모킹 +const mockUser: UserData = { + id: 1, + email: 'test@example.com', + name: 'Test User', + companyName: 'Test Company', + image: 'test-image.jpg', + createdAt: '2024-10-01T00:00:00Z', + updatedAt: '2024-10-03T00:00:00Z', +}; + +// Popup 컴포넌트 모킹 +jest.mock('../Popup/Popup', () => ({ + default: ({ onClickConfirm }: { onClickConfirm: () => void }) => ( +
+

로그인이 필요해요.

+
+ ), +})); + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), + useParams: jest.fn(() => ({ id: '1' })), +})); + +// hook 컴포넌트 모킹 +jest.mock('@/hooks/useCopyUrlToClipboard', () => jest.fn()); +jest.mock('@/hooks/useCancelGathering', () => jest.fn()); +jest.mock('@/hooks/useParticipation', () => jest.fn()); +let isShowPopup = false; + +describe('ParticipationButton', () => { + const mockRouter = { push: jest.fn() }; + const mockCopyUrlToClipboard = jest.fn(); + const mockCancelGathering = jest.fn(); + const mockSetHasParticipated = jest.fn(); + const mockSetIsShowPopup = jest.fn(); + + beforeEach(() => { + (useRouter as jest.Mock).mockReturnValue(mockRouter); + + // useCopyUrlToClipboard의 함수 모킹 + (useCopyUrlToClipboard as jest.Mock).mockReturnValue({ + copyUrlToClipboard: mockCopyUrlToClipboard, + }); + + // useCancelGathering의 함수 모킹 + (useCancelGathering as jest.Mock).mockReturnValue({ + cancelGathering: mockCancelGathering, + }); + + // useParticipation 훅의 반환값 모킹 + (useParticipation as jest.Mock).mockImplementation((user) => ({ + hasParticipated: false, + setHasParticipated: mockSetHasParticipated, + isShowPopup, + setIsShowPopup: mockSetIsShowPopup, // 모킹된 함수 사용 + handleJoinClick: jest.fn(async (value) => { + if (!user) { + isShowPopup = value; + mockSetIsShowPopup(true); + } + }), + })); + }); + + afterEach(() => { + jest.clearAllMocks(); // 모킹된 함수 초기화 + }); + + // 주최자일 때 "취소하기"와 "공유하기" 버튼이 렌더링되는지 확인 + it('checks if "Cancel" and "Share" buttons are rendered when the user is a host', () => { + render( + , + ); + + expect( + screen.getByRole('button', { name: '취소하기' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: '공유하기' }), + ).toBeInTheDocument(); + }); + + // 주최자일 때 "공유하기" 버튼을 클릭하면 copyUrlToClipboard 함수가 호출되는지 확인 + it('checks if the copyUrlToClipboard function is called when "Share" button is clicked by the host', () => { + render( + , + ); + + const shareButton = screen.getByRole('button', { name: '공유하기' }); + fireEvent.click(shareButton); + + expect(mockCopyUrlToClipboard).toHaveBeenCalled(); + }); + + // 주최자일 때 "취소하기" 버튼을 클릭하면 copyUrlToClipboard 함수가 호출되는지 확인 + it('checks if the copyUrlToClipboard function is called when "Cancel" button is clicked by the host', () => { + render( + , + ); + + const cancelButton = screen.getByRole('button', { name: '취소하기' }); + fireEvent.click(cancelButton); + + expect(mockCancelGathering).toHaveBeenCalled(); + }); + + // @todo 테스트코드 수정하기 + // // 주최자일 때 마감일이 지났거나 모임이 취소된 경우 버튼이 비활성화되는지 확인 + // it('checks if buttons are disabled when the host has passed the deadline or the gathering is canceled', () => { + // render( + // , + // ); + + // const cancelButton = screen.getByRole('button', { name: '취소하기' }); + // const shareButton = screen.getByRole('button', { name: '공유하기' }); + + // expect(cancelButton).toBeDisabled(); + // expect(shareButton).toBeDisabled(); + // }); + + // 참여자일 때 참여 상태에 따라 버튼 텍스트가 변경되는지 확인 + it('checks if button text changes based on participation status when the user is a participant', () => { + (useParticipation as jest.Mock).mockReturnValue({ + hasParticipated: true, + setHasParticipated: mockSetHasParticipated, + isShowPopup: false, + setIsShowPopup: mockSetIsShowPopup, + handleJoinClick: jest.fn(), + handleWithdrawClick: jest.fn(), + }); + + render( + , + ); + + expect( + screen.getByRole('button', { name: '참여 취소하기' }), + ).toBeInTheDocument(); + }); + + // 참여 인원이 가득 찼을 때 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when the participant count is full', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); + + // 마감일이 지났을 때 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when the deadline has passed', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); + + // 모임이 취소되었을 때 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when the gathering is canceled', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); + + // 참여 인원이 가득 찼고 마감일이 지난 경우 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when participant count is full and registration end date has passed.', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); + + // 모임이 취소되고 마감일이 지난 경우 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when the meeting is canceled and registration end date has passed.', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); + + // 모든 조건이 충족될 때 버튼이 비활성화되는지 확인 + it('checks if the button is disabled when all conditions are met.', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: '참여하기' }); + expect(button).toBeDisabled(); + }); +}); diff --git a/src/app/components/BottomFloatingBar/ParticipationButton.tsx b/src/app/components/BottomFloatingBar/ParticipationButton.tsx index 379187a8..3bb0ae9c 100644 --- a/src/app/components/BottomFloatingBar/ParticipationButton.tsx +++ b/src/app/components/BottomFloatingBar/ParticipationButton.tsx @@ -87,10 +87,11 @@ const ParticipationButton = ({ // 주최자일 경우 if (isHost) { - const disabled = isRegistrationEnded || isCancelled; // 마감일이 지났거나 취소되었을 경우 button 비활성화 + const disabled = isRegistrationEnded; // 마감일이 지난 경우 버튼 비활성화 + return (
- {renderButton('취소하기', 'white', cancelGathering, disabled)} + {renderButton('취소하기', 'white', cancelGathering)} {renderButton('공유하기', 'default', copyUrlToClipboard, disabled)}
); diff --git a/src/app/components/Card/Card.tsx b/src/app/components/Card/Card.tsx index 9615b68d..75117961 100644 --- a/src/app/components/Card/Card.tsx +++ b/src/app/components/Card/Card.tsx @@ -12,6 +12,7 @@ import { formatDate, formatTime } from '@/utils/formatDate'; import { UserJoinedGatheringsData } from '@/types/data.type'; import { createContext, PropsWithChildren, useContext } from 'react'; import { MIN_PARTICIPANTS } from '@/constants/common'; +import Link from 'next/link'; interface CardProps { data: UserJoinedGatheringsData; @@ -32,16 +33,18 @@ const Card = ({
{/* 이미지 */} -
- 모임 이미지 -
+ +
+ 모임 이미지 +
+ {/* content - chip, info, button */} @@ -131,7 +134,7 @@ const CardInfo = (): JSX.Element => { const { name, location, dateTime, participantCount, capacity } = data; return ( - <> +

{name}

| @@ -148,7 +151,7 @@ const CardInfo = (): JSX.Element => { {participantCount}/{capacity}

- + ); }; diff --git a/src/app/components/CardList/CardList.test.tsx b/src/app/components/CardList/CardList.test.tsx index dd5f7acb..feec2f9c 100644 --- a/src/app/components/CardList/CardList.test.tsx +++ b/src/app/components/CardList/CardList.test.tsx @@ -2,12 +2,12 @@ import React, { MouseEventHandler } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import CardList from './CardList'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; const date = new Date(); date.setDate(date.getDate() + 3); -const MOCK_DATA_BASE: GatheringsListData = { +const MOCK_DATA_BASE: GatheringType = { teamId: 1, id: 1, type: 'test', diff --git a/src/app/components/CardList/CardList.tsx b/src/app/components/CardList/CardList.tsx index cca64782..2d65de58 100644 --- a/src/app/components/CardList/CardList.tsx +++ b/src/app/components/CardList/CardList.tsx @@ -13,7 +13,7 @@ import { formatTimeHours, isSameDate, } from '@/utils/formatDate'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import Tag from '@/app/components/Tag/Tag'; import InfoChip from '@/app/components/Chip/InfoChip'; import ProgressBar from '@/app/components/ProgressBar/ProgressBar'; @@ -21,7 +21,7 @@ import { MouseEvent, useState } from 'react'; // TODO : optional props를 필수로 변경 interface CardProps { - data: GatheringsListData; + data: GatheringType; isSaved?: boolean; handleButtonClick?: (id: number) => void; } diff --git a/src/app/components/Gnb/TokenExpirationTimerLayout.tsx b/src/app/components/Gnb/TokenExpirationTimerLayout.tsx index edbd2d13..9adc088e 100644 --- a/src/app/components/Gnb/TokenExpirationTimerLayout.tsx +++ b/src/app/components/Gnb/TokenExpirationTimerLayout.tsx @@ -3,7 +3,7 @@ import { TokenExpirationTimer } from '@/utils/TokenExpirationTimer'; const TimerStyle = { - gnb: 'hidden text-14 font-semibold text-var-orange-50 md:block md:text-16', + gnb: 'hidden text-14 font-semibold text-var-orange-50 md:block md:text-16 w-140', dropdown: 'px-16 py-12 text-[12px] font-medium text-var-black hover:bg-var-orange-100 md:px-16', }; diff --git a/src/app/components/InformationCard/Avatars.tsx b/src/app/components/InformationCard/Avatars.tsx new file mode 100644 index 00000000..5dffc481 --- /dev/null +++ b/src/app/components/InformationCard/Avatars.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; + +import Avatar from './Avatar'; +import { GatheringParticipantsType } from '@/types/data.type'; + +interface AvatarsProps { + participants: GatheringParticipantsType[]; + participantCount: number; +} + +const MAX_VISIBLE_AVATAR = 4; + +const Avatars = ({ participants, participantCount }: AvatarsProps) => { + const [isHovered, setIsHovered] = useState(false); + + const maxVisible = MAX_VISIBLE_AVATAR; + const visibleAvatars = participants + .slice(0, maxVisible) + .map(({ User }) => ( + + )); + + if (participantCount > maxVisible) { + visibleAvatars.push( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ +{participantCount - maxVisible} +
+
+ {participants.slice(maxVisible).map(({ User }) => ( + + ))} +
+
, + ); + } + + return <>{visibleAvatars}; +}; + +export default Avatars; diff --git a/src/app/components/InformationCard/CapacityInfo.tsx b/src/app/components/InformationCard/CapacityInfo.tsx new file mode 100644 index 00000000..332f51f1 --- /dev/null +++ b/src/app/components/InformationCard/CapacityInfo.tsx @@ -0,0 +1,31 @@ +import ProgressBar from '../ProgressBar/ProgressBar'; +import { MIN_PARTICIPANTS } from '@/constants/common'; + +interface CapacityInfoProps { + participantCount: number; + maxParticipants: number; +} + +const CapacityInfo = ({ + participantCount, + maxParticipants, +}: CapacityInfoProps) => { + return ( +
+ + +
+
최소 인원 {MIN_PARTICIPANTS}명
+
최대 인원 {maxParticipants}명
+
+
+ ); +}; + +export default CapacityInfo; diff --git a/src/app/components/InformationCard/DateTimeChips.tsx b/src/app/components/InformationCard/DateTimeChips.tsx new file mode 100644 index 00000000..b15066ea --- /dev/null +++ b/src/app/components/InformationCard/DateTimeChips.tsx @@ -0,0 +1,22 @@ +import InfoChip from '../Chip/InfoChip'; +import { formatDate, formatTimeColon } from '@/utils/formatDate'; + +interface DateTimeChipsProps { + date: string; + time: string; +} + +const DateTimeChips = ({ date, time }: DateTimeChipsProps) => { + return ( +
+ + {formatDate(date)} + + + {formatTimeColon(time)} + +
+ ); +}; + +export default DateTimeChips; diff --git a/src/app/components/InformationCard/InformationCard.tsx b/src/app/components/InformationCard/InformationCard.tsx index 44fe073d..da4ab7b7 100644 --- a/src/app/components/InformationCard/InformationCard.tsx +++ b/src/app/components/InformationCard/InformationCard.tsx @@ -1,20 +1,12 @@ 'use client'; -import { useState } from 'react'; import { useParams } from 'next/navigation'; -import { - IconCheckCircle, - IconSaveActive, - IconSaveInactive, -} from '@/public/icons'; -import Avatar from './Avatar'; -import InfoChip from '../Chip/InfoChip'; -import ProgressBar from '../ProgressBar/ProgressBar'; import { GatheringParticipantsType } from '@/types/data.type'; -import { MIN_PARTICIPANTS } from '@/constants/common'; -import { formatDate, formatTimeColon } from '@/utils/formatDate'; -import { useSavedGatheringList } from '@/context/SavedGatheringContext'; +import InformationCardHeader from './InformationCardHeader'; +import DateTimeChips from './DateTimeChips'; +import ParticipantsInfo from './ParticipantsInfo'; +import CapacityInfo from './CapacityInfo'; interface InformationCardProps { title: string; @@ -38,160 +30,25 @@ const InformationCard = ({ const params = useParams(); const gatheringId = Number(params.id); - const { savedGatherings, updateGathering } = useSavedGatheringList(); - - const checkGatheringSaved = (id: number | undefined, savedList: number[]) => { - return id ? savedList.includes(id) : false; - }; - - const [isSaved, setIsSaved] = useState(() => - checkGatheringSaved(gatheringId, savedGatherings), - ); - const [isHovered, setIsHovered] = useState(false); - - const handleToggleSave = () => { - setIsSaved((prev) => !prev); - if (gatheringId) { - updateGathering(gatheringId); - } - }; - - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - // function of setting Avatars with remaining - const renderAvatars = () => { - const maxVisible = 4; - const visibleAvatars = participants - .slice(0, maxVisible) - .map(({ User }) => ( - - )); - - if (participantCount > maxVisible) { - visibleAvatars.push( -
-
- +{participantCount - maxVisible} -
- -
- {participants.slice(maxVisible).map(({ User }) => ( - - ))} -
-
, - ); - } - - return visibleAvatars; - }; - return (
-
-
-
- {title} -
-
- {address} -
-
- - {/* 찜 */} - {isSaved ? ( - - ) : ( - - )} -
- - {/* 날짜, 시간 chip */} -
- - {formatDate(date)} - - - {formatTimeColon(time)} - -
+ +
- - {/* 모집 정원 */}
-
-
-
- 모집 정원 {participantCount}명 -
-
{renderAvatars()}
-
-
- {participantCount >= MIN_PARTICIPANTS && ( - <> - -
- 개설확정 -
- - )} -
-
- - {/* progress bar */} - + - -
-
최소인원 {MIN_PARTICIPANTS}명
-
- 최대인원 {maxParticipants}명 -
-
); diff --git a/src/app/components/InformationCard/InformationCardHeader.tsx b/src/app/components/InformationCard/InformationCardHeader.tsx new file mode 100644 index 00000000..0067df69 --- /dev/null +++ b/src/app/components/InformationCard/InformationCardHeader.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; + +import { IconSaveActive, IconSaveInactive } from '@/public/icons'; +import { useSavedGatheringList } from '@/context/SavedGatheringContext'; + +interface InformationCardHeaderProps { + title: string; + address: string; + gatheringId: number; +} + +const InformationCardHeader = ({ + title, + address, + gatheringId, +}: InformationCardHeaderProps) => { + const { savedGatherings, updateGathering } = useSavedGatheringList(); + + const checkGatheringSaved = (id: number | undefined, savedList: number[]) => { + return id ? savedList.includes(id) : false; + }; + + const [isSaved, setIsSaved] = useState(() => + checkGatheringSaved(gatheringId, savedGatherings), + ); + + const handleToggleSave = () => { + setIsSaved((prev) => !prev); + if (gatheringId) { + updateGathering(gatheringId); + } + }; + + return ( +
+
+
+ {title} +
+
+ {address} +
+
+ + {isSaved ? ( + + ) : ( + + )} +
+ ); +}; + +export default InformationCardHeader; diff --git a/src/app/components/InformationCard/ParticipantsInfo.tsx b/src/app/components/InformationCard/ParticipantsInfo.tsx new file mode 100644 index 00000000..71ab27e2 --- /dev/null +++ b/src/app/components/InformationCard/ParticipantsInfo.tsx @@ -0,0 +1,42 @@ +import Avatars from './Avatars'; +import { IconCheckCircle } from '@/public/icons'; +import { GatheringParticipantsType } from '@/types/data.type'; +import { MIN_PARTICIPANTS } from '@/constants/common'; + +interface ParticipantsInfoProps { + participants: GatheringParticipantsType[]; + participantCount: number; +} + +const ParticipantsInfo = ({ + participants, + participantCount, +}: ParticipantsInfoProps) => { + return ( +
+
+
+ 현재 인원 {participantCount}명 +
+
+ +
+
+ + {/* 개설 확정 여부 */} + {participantCount >= MIN_PARTICIPANTS && ( +
+ +
+ 개설확정 +
+
+ )} +
+ ); +}; + +export default ParticipantsInfo; diff --git a/src/app/components/InformationCard/infoCard.tsx b/src/app/components/InformationCard/infoCard.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/Modal/MakeGatheringModal.tsx b/src/app/components/Modal/MakeGatheringModal.tsx index 0ae1ca81..9cdd77bd 100644 --- a/src/app/components/Modal/MakeGatheringModal.tsx +++ b/src/app/components/Modal/MakeGatheringModal.tsx @@ -14,6 +14,7 @@ import RecruitmentNumber from './MakeGatheringModal/RecruitmentNumber'; import SelectTimeChip from './MakeGatheringModal/SelectTimeChip'; import ModalFrame from './ModalFrame'; import ModalHeader from './ModalHeader'; +import NameInput from './MakeGatheringModal/NameInput'; interface MakeGatheringModalProps { onClose: () => void; @@ -22,6 +23,7 @@ interface MakeGatheringModalProps { const MakeGatheringModal = ({ onClose }: MakeGatheringModalProps) => { const router = useRouter(); + const [name, setName] = useState(''); const [location, setLocation] = useState(null); const [image, setImage] = useState(null); const [gatheringType, setGatheringType] = useState>({ @@ -55,6 +57,7 @@ const MakeGatheringModal = ({ onClose }: MakeGatheringModalProps) => { } const isFormValid = () => + name && location && image && !isAllGatheringTypeFalse() && @@ -71,6 +74,7 @@ const MakeGatheringModal = ({ onClose }: MakeGatheringModalProps) => { } const formData = new FormData(); + formData.append('name', name); formData.append('location', location!); formData.append('type', getSelectedGatheringType()); formData.append('dateTime', (combinedDateTime as Date).toISOString()); @@ -90,7 +94,6 @@ const MakeGatheringModal = ({ onClose }: MakeGatheringModalProps) => { router.push(`/gatherings/${data.id}`); toast.success(message); - // TODO : 모임 생성 후 페이지 리로드 }; return ( @@ -103,6 +106,9 @@ const MakeGatheringModal = ({ onClose }: MakeGatheringModalProps) => { {/* 헤더 */} + {/* 모임 이름 */} + + {/* 장소 */} >; +} +const NameInput = ({ setName }: NameInputProps) => { + return ( +
+

이름

+ setName(e.target.value)} + /> +
+ ); +}; + +export default NameInput; diff --git a/src/app/components/UserProfileLayout/UserInfo.test.tsx b/src/app/components/UserProfileLayout/UserInfo.test.tsx new file mode 100644 index 00000000..e206844c --- /dev/null +++ b/src/app/components/UserProfileLayout/UserInfo.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@testing-library/react'; +import UserInfo from './UserInfo'; +import '@testing-library/jest-dom'; +import { UserData } from '@/types/client.type'; + +describe('UserInfo', () => { + const mockUser: UserData = { + id: '1', + email: 'test@example.com', + name: 'Test User', + companyName: 'Test Company', + image: 'test-image.jpg', + createdAt: '2024-10-01T00:00:00Z', + updatedAt: '2024-10-03T00:00:00Z', + }; + + // 유저 정보가 정상적으로 렌더링 + it('renders user information correctly', () => { + // 컴포넌트를 렌더링할 때 mockUser를 prop으로 전달 + render(); + + // 이름, 회사명, 이메일이 올바르게 화면에 표시되는지 확인 + expect(screen.getByText('Test User')).toBeInTheDocument(); + expect(screen.getByText('Test Company')).toBeInTheDocument(); + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + }); + + // user prop이 null일 때 아무것도 렌더링 안됨 + it('renders nothing when user prop is null', () => { + // user가 null인 경우로 렌더링 + render(); + + // 유저 정보가 존재하지 않으므로 해당 요소들이 화면에 없는지 확인 + expect(screen.queryByText('Test User')).not.toBeInTheDocument(); + expect(screen.queryByText('Test Company')).not.toBeInTheDocument(); + expect(screen.queryByText('test@example.com')).not.toBeInTheDocument(); + }); +}); diff --git a/src/app/components/UserProfileLayout/UserProfileHeader.test.tsx b/src/app/components/UserProfileLayout/UserProfileHeader.test.tsx new file mode 100644 index 00000000..e0110564 --- /dev/null +++ b/src/app/components/UserProfileLayout/UserProfileHeader.test.tsx @@ -0,0 +1,44 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import UserProfileHeader from './UserProfileHeader'; +import '@testing-library/jest-dom'; + +// 이미지 모킹 +jest.mock('@/public/images', () => ({ + BtnEdit: () =>
, + ImageProfile: () =>
, +})); + +describe('UserProfileHeader', () => { + const mockToggleModal = jest.fn(); + + beforeEach(() => { + render(); + }); + + // 기본 렌더링 + it('renders correctly', () => { + // 헤더 이미지가 렌더링 되었는지 확인 + const profileImage = screen.getByTestId('ImageProfile'); + expect(profileImage).toBeInTheDocument(); + + // 프로필수정 버튼이 렌더링 되었는지 확인 + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + + // 버튼아이콘이 렌더링 되었는지 확인 + const buttonIcon = screen.getByTestId('BtnEdit'); + expect(buttonIcon).toBeInTheDocument(); + }); + + // 버튼 클릭 시 toggleModal 함수가 호출되는지 확인 + it('calls toggleModal when button is clicked', () => { + // 버튼 찾기 + const button = screen.getByRole('button'); + + // 버튼 클릭 + fireEvent.click(button); + + // toggleModal 함수가 한 번 호출되었는지 확인 + expect(mockToggleModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/components/UserProfileLayout/UserProfileLayout.test.tsx b/src/app/components/UserProfileLayout/UserProfileLayout.test.tsx new file mode 100644 index 00000000..80eb0e95 --- /dev/null +++ b/src/app/components/UserProfileLayout/UserProfileLayout.test.tsx @@ -0,0 +1,97 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import UserProfileLayout from './UserProfileLayout'; +import { UserData } from '@/types/client.type'; +import '@testing-library/jest-dom'; +import { Dispatch, SetStateAction } from 'react'; + +// 프로필수정 모달 모킹 +jest.mock('../Modal/ProfileEditModal', () => { + function MockProfileEditModal({ + onClose, + onUploadProfileImage, + onSubmit, + profileInput, + setProfileInput, + }: { + user: UserData | null; + onClose: () => void; + onUploadProfileImage?: () => void; + onSubmit?: () => void; + imagePreview?: string; + profileInput: string; + setProfileInput: Dispatch>; + }) { + return ( +
+

프로필 수정 모달

+ + + + setProfileInput(e.target.value)} + /> +
+ ); + } + + MockProfileEditModal.displayName = 'MockProfileEditModal'; + return MockProfileEditModal; +}); + +// UserProfileHeader 모킹 +jest.mock('./UserProfileHeader', () => { + const MockUserProfileHeader = ({ + toggleModal, + }: { + toggleModal: () => void; + }) => ( +
+
UserProfileHeader Mock
+ +
+ ); + + MockUserProfileHeader.displayName = 'MockUserProfileHeader'; + return MockUserProfileHeader; +}); + +const mockUser: UserData = { + id: '1', + email: 'test@example.com', + name: 'Test User', + companyName: 'Test Company', + image: '/test-image.jpg', + createdAt: '2024-10-01T00:00:00Z', + updatedAt: '2024-10-03T00:00:00Z', +}; + +describe('UserProfileLayout', () => { + beforeEach(() => { + render(); + }); + + // 기본 렌더링 + it('render correctly', () => { + expect(screen.getByText('Test User')).toBeInTheDocument(); + expect(screen.getByText('Test Company')).toBeInTheDocument(); + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + expect(screen.getByAltText('Profile')).toBeInTheDocument(); + }); + + // 모달 열기 및 닫기 테스트 + it('opens and closes the ProfileEditModal', async () => { + // 모달 열기 버튼 클릭 + fireEvent.click(screen.getByRole('button', { name: 'Edit' })); + + // 모달이 열렸는지 확인 + expect(await screen.findByText('프로필 수정 모달')).toBeInTheDocument(); + + // 모달 닫기 버튼 클릭 + fireEvent.click(screen.getByRole('button', { name: '닫기' })); + + // 모달이 닫혔는지 확인 + expect(screen.queryByText('프로필 수정 모달')).not.toBeInTheDocument(); + }); +}); diff --git a/src/app/components/UserProfileLayout/useProfileState.test.ts b/src/app/components/UserProfileLayout/useProfileState.test.ts index 9b42aec7..6f0b8d6d 100644 --- a/src/app/components/UserProfileLayout/useProfileState.test.ts +++ b/src/app/components/UserProfileLayout/useProfileState.test.ts @@ -1,9 +1,8 @@ -// useProfileState.test.ts import { renderHook, act } from '@testing-library/react'; import { useProfileState } from './useProfileState'; import { UserData } from '@/types/client.type'; -describe('useProfileState 훅 테스트', () => { +describe('useProfileState', () => { // 테스트 전에 사용할 기본 유저 데이터 let user: UserData; diff --git a/src/app/loading.tsx b/src/app/loading.tsx index 50e35ca6..8a1d4420 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -5,7 +5,7 @@ import Loader from '@/app/components/Loader/Loader'; const Loading = () => { return (
- ; +
); }; diff --git a/src/hooks/useFetchFilteredGatherings.ts b/src/hooks/useFetchFilteredGatherings.ts index 87645b77..8f3713fc 100644 --- a/src/hooks/useFetchFilteredGatherings.ts +++ b/src/hooks/useFetchFilteredGatherings.ts @@ -24,7 +24,7 @@ const useFetchFilteredGatherings = ( ? formatCalendarDate(selectedDate) : undefined; const sortBy = overrides.sortBy || sortOption; - const sortOrder = sortBy ? 'desc' : undefined; + const sortOrder = sortBy === 'registrationEnd' ? 'asc' : 'desc'; const newData = await getGatherings({ type, diff --git a/src/hooks/useGatherings.ts b/src/hooks/useGatherings.ts index 880355c8..27950566 100644 --- a/src/hooks/useGatherings.ts +++ b/src/hooks/useGatherings.ts @@ -3,7 +3,7 @@ * 이 훅은 초기 모임 데이터를 상태로 관리하며, 다양한 필터와 정렬 옵션을 설정할 수 있는 핸들러 함수를 제공합니다. * 또한, 필터 변경 시 쿼리 파라미터를 업데이트하고, 필터링된 데이터를 서버에서 가져오는 기능을 포함합니다. * - * @param {GatheringsListData[]} initialGatherings - 초기 모임 데이터 배열 + * @param {GatheringType[]} initialGatherings - 초기 모임 데이터 배열 */ import { useEffect, useState } from 'react'; @@ -18,11 +18,11 @@ import { GatheringFilters, GatheringTabsType, } from '@/types/client.type'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; -const useGatherings = (initialGatherings: GatheringsListData[]) => { +const useGatherings = (initialGatherings: GatheringType[]) => { const [filteredData, setFilteredData] = - useState(initialGatherings); + useState(initialGatherings); const [activeTab, setActiveTab] = useState('DALLAEMFIT'); const [selectedLocation, setSelectedLocation] = useState( diff --git a/src/hooks/useLoadMore.ts b/src/hooks/useLoadMore.ts index b5ba95c3..c6f6025c 100644 --- a/src/hooks/useLoadMore.ts +++ b/src/hooks/useLoadMore.ts @@ -1,16 +1,16 @@ import { useState } from 'react'; import { LIMIT_PER_REQUEST } from '@/constants/common'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; const useLoadMore = ( fetchFilteredGatherings: (filters: { offset: number; - }) => Promise, + }) => Promise, offset: number, setOffset: (offset: number) => void, setFilteredData: ( - data: (prevData: GatheringsListData[]) => GatheringsListData[], + data: (prevData: GatheringType[]) => GatheringType[], ) => void, ) => { const [isLoading, setIsLoading] = useState(false); diff --git a/src/hooks/useSavedGatherings.ts b/src/hooks/useSavedGatherings.ts index aa857464..1ccd1134 100644 --- a/src/hooks/useSavedGatherings.ts +++ b/src/hooks/useSavedGatherings.ts @@ -1,14 +1,14 @@ import getGatherings from '@/app/api/actions/gatherings/getGatherings'; import { SORT_OPTIONS_MAP } from '@/constants/common'; import { GatheringChipsType, GatheringTabsType } from '@/types/client.type'; -import { GatheringsListData, GetGatheringsParams } from '@/types/data.type'; +import { GatheringType, GetGatheringsParams } from '@/types/data.type'; import { formatCalendarDate } from '@/utils/formatDate'; import { useEffect, useState } from 'react'; const useSavedGatherings = (savedGatherings: number[]) => { - const [gatheringListData, setGatheringListData] = useState< - GatheringsListData[] - >([]); + const [gatheringListData, setGatheringListData] = useState( + [], + ); const [activeTab, setActiveTab] = useState('DALLAEMFIT'); const [activeChip, setActiveChip] = useState('ALL'); diff --git a/src/hooks/useUserCreated.ts b/src/hooks/useUserCreated.ts index cca95158..a6b349ab 100644 --- a/src/hooks/useUserCreated.ts +++ b/src/hooks/useUserCreated.ts @@ -1,6 +1,6 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import getGatherings from '@/app/api/actions/gatherings/getGatherings'; -import { GatheringsListData } from '@/types/data.type'; +import { GatheringType } from '@/types/data.type'; import { DEFAULT_OFFSET, LIMIT_PER_REQUEST, @@ -8,7 +8,7 @@ import { } from '@/constants/common'; export const useUserCreated = ( - initialGatheringList: GatheringsListData[], + initialGatheringList: GatheringType[], createdBy: string, ) => { const { diff --git a/src/styles/globals.css b/src/styles/globals.css index 73ac91d3..1014a17c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,78 +2,7 @@ @tailwind components; @tailwind utilities; -@font-face { - font-family: 'Pretendard'; - font-weight: 100; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Thin.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 200; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-ExtraLight.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 300; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Light.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 400; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Regular.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 500; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Medium.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 600; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-SemiBold.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 700; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Bold.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 800; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-ExtraBold.woff') - format('woff'); - font-display: swap; -} -@font-face { - font-family: 'Pretendard'; - font-weight: 900; - font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/pretendard/Pretendard-Black.woff') - format('woff'); - font-display: swap; -} +@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css'); /* 사용자 정의 스크롤 바 숨기기 클래스 */ .scrollbar-hide { diff --git a/src/types/data.type.ts b/src/types/data.type.ts index 95705ae4..21eab9c0 100644 --- a/src/types/data.type.ts +++ b/src/types/data.type.ts @@ -1,8 +1,7 @@ import { GatheringsType } from './client.type'; -// GET : /{teamId}/gatherings/joined -// 로그인된 사용자가 참석한 모임 목록 조회 시 Response Data -export interface UserJoinedGatheringsData { +// GET : /{teamId}/gatherings +export interface GatheringType { teamId: number | string; id: number; type: string; @@ -15,44 +14,20 @@ export interface UserJoinedGatheringsData { image: string; createdBy: number; canceledAt?: string | null; +} + +// GET : /{teamId}/gatherings/joined +export interface UserJoinedGatheringsData extends GatheringType { joinedAt?: string; isCompleted?: boolean; isReviewed?: boolean; } -// GET : /{teamId}/gatherings -// 모임 목록 조회 시 Response Data -export interface GatheringsListData { - teamId: number | string; - id: number; - type: string; - name: string; - dateTime: string; - registrationEnd: string; - location: string; - participantCount: number; - capacity: number; - image: string; - createdBy: number; - canceledAt?: string | null; -} - // GET : /{teamId}/gatherings/{gatheringId} // POST: /{teamId}/gatherings -export interface GatheringInfoType { - teamId: number | string; - id: number; - type: string; +export type GatheringInfoType = Omit & { name: string | null; - dateTime: string; - registrationEnd: string; - location: string; - participantCount: number; - capacity: number; - image: string; - createdBy: number; - canceledAt?: string | null; -} +}; // GET : /{teamId}/gatherings/{id}/participants export interface GatheringParticipantsType { @@ -70,12 +45,12 @@ export interface GatheringParticipantsType { } // GET : /{teamId}/gatherings/joined -// 참석한 모임 목록 조회 시 Response Data -export interface myGatheringData extends GatheringsListData { +export interface myGatheringData extends GatheringType { joinedAt: string; isCompleted: boolean; isReviewed: boolean; } + // GET : mockdata // 무한스크롤 구현을 위한 mock data의 인터페이스입니다. 수정 및 삭제될 수 있음 export interface FetchGatheringsResponse {