diff --git a/src/_apis/liked/liked-apis.ts b/src/_apis/liked/liked-apis.ts new file mode 100644 index 00000000..c48825c6 --- /dev/null +++ b/src/_apis/liked/liked-apis.ts @@ -0,0 +1,39 @@ +import { fetchApi } from '@/src/utils/api'; +import { GatheringResponseType } from '@/src/types/gathering-data'; + +// 찜 목록 조회 +export async function getLikedList(page: number): Promise { + const url = `/api/liked/memberLikes?page=${page}&size=6`; + + const response = await fetchApi<{ data: GatheringResponseType }>(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; +} + +// 찜 추가하기 +export async function addLike(gatheringId: number): Promise { + const url = `/api/liked/${gatheringId}`; + + await fetchApi(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +// 찜 해제하기 +export async function removeLike(gatheringId: number): Promise { + const url = `/api/liked/${gatheringId}`; + + await fetchApi(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/src/_queries/liked/liked-queries.ts b/src/_queries/liked/liked-queries.ts new file mode 100644 index 00000000..4f280e48 --- /dev/null +++ b/src/_queries/liked/liked-queries.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getLikedList } from '@/src/_apis/liked/liked-apis'; + +export function useGetLikedListQuery(page: number) { + return useQuery({ + queryKey: ['likedList', page], + queryFn: () => getLikedList(page), + }); +} diff --git a/src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx b/src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx index 35b3a01a..1851a336 100644 --- a/src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx +++ b/src/app/(crew)/crew/_components/gathering-detail-modal/container.tsx @@ -1,12 +1,12 @@ 'use client'; +import { toast } from 'react-toastify'; import { CancelGathering, JoinGathering, LeaveGathering, } from '@/src/_apis/gathering/gathering-detail-apis'; import { ApiError } from '@/src/utils/api'; -import Toast from '@/src/components/common/toast'; import { GatheringDetailType } from '@/src/types/gathering-data'; import GatheringDetailModalPresenter from './presenter'; @@ -24,7 +24,7 @@ export default function GatheringDetailModalContainer({ data, }: GatheringDetailModalContainerProps) { const showToast = (message: string, type: 'success' | 'error' | 'warning') => { - Toast({ message, type }); + toast(message, { type }); }; const handleJoin = async () => { diff --git a/src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx index 89c86615..70594e7e 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx @@ -1,6 +1,12 @@ 'use client'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/navigation'; +import { addLike, removeLike } from '@/src/_apis/liked/liked-apis'; import { useGetGatheringListQuery } from '@/src/_queries/crew/gathering-list-queries'; +import { ApiError } from '@/src/utils/api'; +import ConfirmModal from '@/src/components/common/modal/confirm-modal'; import GatheringCardCarousel from '@/src/components/gathering-list/gathering-card-carousel'; interface GatheringListSectionProps { @@ -9,12 +15,57 @@ interface GatheringListSectionProps { export default function GatheringListSection({ id }: GatheringListSectionProps) { const { data: gatheringList, isLoading, error } = useGetGatheringListQuery(id); + const [showLoginModal, setShowLoginModal] = useState(false); + const router = useRouter(); - if (isLoading) return

로딩 중...

; + const handleLike = async (gatheringId: number) => { + try { + await addLike(gatheringId); + } catch (apiError) { + if (apiError instanceof ApiError) { + toast.error(`찜하기에 실패했습니다: ${apiError.message}`); + } + } + }; + + const handleUnlike = async (gatheringId: number) => { + try { + await removeLike(gatheringId); + } catch (apiError) { + if (apiError instanceof ApiError) { + toast.error(`찜하기 해제에 실패했습니다: ${apiError.message}`); + } + } + }; - if (error) return

데이터를 불러오는 데 실패했습니다: {error.message}

; + const handleLoginRedirect = () => { + const currentPath = window.location.pathname || '/'; + router.push(`/login?redirect=${encodeURIComponent(currentPath)}`); + }; + // TODO: 추후 에러, 로딩 수정 + if (isLoading) return

로딩 중...

; + if (error) return

데이터를 불러오는 데 실패했습니다

; if (!gatheringList || gatheringList.length === 0) return

데이터가 없습니다.

; - return ; + return ( + <> + setShowLoginModal(true)} + /> + {showLoginModal && ( + setShowLoginModal(false)} + onConfirm={handleLoginRedirect} + > + 로그인이 필요합니다! + + )} + + ); } diff --git a/src/app/(crew)/layout.tsx b/src/app/(crew)/layout.tsx index dd0adbf3..56b0e955 100644 --- a/src/app/(crew)/layout.tsx +++ b/src/app/(crew)/layout.tsx @@ -1,3 +1,5 @@ +import { Bounce, ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import '@mantine/core/styles.css'; import Header from '@/src/components/common/header/container'; import '@/src/styles/globals.css'; @@ -10,6 +12,19 @@ export default function RootLayout({ return ( <>
+
{children} diff --git a/src/app/(crew)/my-favorite/page.tsx b/src/app/(crew)/my-favorite/page.tsx index b28044d2..4bebe141 100644 --- a/src/app/(crew)/my-favorite/page.tsx +++ b/src/app/(crew)/my-favorite/page.tsx @@ -1,10 +1,9 @@ -import GatheringList from '@/src/components/gathering-list/gathering-list'; -import { gatheringData } from '@/src/mock/gathering-data'; +import LikedList from '@/src/components/gathering-list/liked-list-container'; export default function FavoritePage() { return (
- +
); } diff --git a/src/app/api/test-api/route.ts b/src/app/api/test-api/route.ts deleted file mode 100644 index d773e8fe..00000000 --- a/src/app/api/test-api/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse } from 'next/server'; - -// FIX: react-query로 임시로 작성된 코드입니다. 추후 삭제 - -export async function GET() { - const users = [ - { - id: 1, - name: 'A', - profile_url: 'https://i.pinimg.com/736x/e4/4a/09/e44a09cd9a4890667a6d04912055a430.jpg', - }, - { - id: 2, - name: 'B', - profile_url: 'https://i.pinimg.com/736x/e4/4a/09/e44a09cd9a4890667a6d04912055a430.jpg', - }, - { - id: 3, - name: 'C', - profile_url: 'https://i.pinimg.com/736x/e4/4a/09/e44a09cd9a4890667a6d04912055a430.jpg', - }, - ]; - - return NextResponse.json({ data: users }); -} diff --git a/src/components/common/gathering-card/container.tsx b/src/components/common/gathering-card/container.tsx index cc4000f1..40505485 100644 --- a/src/components/common/gathering-card/container.tsx +++ b/src/components/common/gathering-card/container.tsx @@ -1,17 +1,19 @@ 'use client'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useDisclosure } from '@mantine/hooks'; import { useGetGatheringDetailQuery } from '@/src/_queries/gathering/gathering-detail-queries'; import { ApiError } from '@/src/utils/api'; import GatheringDetailModalContainer from '@/src/app/(crew)/crew/_components/gathering-detail-modal/container'; -import Toast from '@/src/components/common/toast'; import { GatheringType } from '@/src/types/gathering-data'; import GatheringCardPresenter from './presenter'; interface GatheringCardContainerProps extends GatheringType { className?: string; crewId: number; + onLike: (gatheringId: number) => Promise; + onUnlike: (gatheringId: number) => Promise; } export default function GatheringCard({ @@ -25,9 +27,10 @@ export default function GatheringCard({ liked: initialIsLiked, className, crewId, + onLike, + onUnlike, }: GatheringCardContainerProps) { const [opened, { open, close }] = useDisclosure(false); - // 임시 찜하기 const [isLiked, setIsLiked] = useState(initialIsLiked); // 날짜 비교 @@ -42,9 +45,19 @@ export default function GatheringCard({ // 마감 시간 문자열 생성 const deadlineMessage = `오늘 ${gatheringDate.getHours()}시 마감`; - // 추후 찜하기 컴포넌트 작성되면 수정 - const handleLikeToggle = () => { - setIsLiked((prev) => !prev); + // 찜하기 상태 업데이트 + const handleLikeToggle = async () => { + try { + if (isLiked) { + await onUnlike(id); + setIsLiked(false); + } else { + await onLike(id); + setIsLiked(true); + } + } catch (error) { + toast.error('찜 상태를 업데이트하는 데 실패했습니다.'); + } }; const { data: gatheringData, error } = useGetGatheringDetailQuery(crewId, id); @@ -56,13 +69,13 @@ export default function GatheringCard({ const errorData = JSON.parse(error.message); if (errorData.status === 'NOT_FOUND') { - Toast({ message: '모임 정보를 찾을 수 없습니다.', type: 'error' }); + toast.error('모임 정보를 찾을 수 없습니다.'); } } catch { - Toast({ message: `Error ${error.status}: ${error.message}`, type: 'error' }); + toast.error(`Error ${error.status}: ${error.message}`); } } else { - Toast({ message: '데이터 통신에 실패했습니다.', type: 'error' }); + toast.error('데이터 통신에 실패했습니다.'); } } }, [error]); diff --git a/src/components/common/modal/confirm-modal.tsx b/src/components/common/modal/confirm-modal.tsx index f5a4ace6..2a061d76 100644 --- a/src/components/common/modal/confirm-modal.tsx +++ b/src/components/common/modal/confirm-modal.tsx @@ -36,7 +36,7 @@ export default function ConfirmModal({ children, opened, onClose, onConfirm }: C opened={opened} onClose={onClose} centered - withCloseButton + withCloseButton={false} size="xs" styles={{ content: { boxShadow: '0 25px 50px -12px rgba(0,0,0,0.1)', borderRadius: '12px' }, @@ -46,8 +46,8 @@ export default function ConfirmModal({ children, opened, onClose, onConfirm }: C blur: 2, }} > -
- {children} +
+
{children}