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 (
{tabList.map((item, index) => (
- -
- {item.name}
+
-
+ {item.name}
))}
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 {