diff --git a/src/api/mypage/api.ts b/src/api/mypage/api.ts deleted file mode 100644 index d36d915b..00000000 --- a/src/api/mypage/api.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { API_PATH } from '@/constants/apiPath'; -import { - LeaveJoinSocialRequest, - GetJoinedSocialListParams, - JoinedSocialResponse, -} from './type'; -import instance from '@/api/instance'; -import { getFilterParams } from '@/utils/getFilterParams'; -import instanceBaaS from '@/api/instanceBaaS'; - -export const getJoinedSocialList = async ({ - limit = 12, - offset = 0, - ...restParams -}: GetJoinedSocialListParams) => { - try { - const response = await instance.get( - `${API_PATH.SOCIAL}/joined?${getFilterParams({ - limit, - offset, - ...restParams, - })}` - ); - return response.data; - } catch (error) { - throw error; - } -}; - -export const getStoryBySocialId = async ( - socialId: number -): Promise => { - const { data, error } = await instanceBaaS - .from('Stories') - .select('story_id') - .eq('social_id', socialId) - .maybeSingle(); - - if (error) throw new Error(error.message); - return data!.story_id; -}; - -export const getCollaboratorsByStoryId = async (storyId: string) => { - const { data, error } = await instanceBaaS - .from('story_collaborators') - .select('*') - .eq('story_id', storyId); - - if (error) throw new Error(error.message); - return data; -}; - -export const deleteCollaboratorFromSocial = async ( - userId: number, - storyId: string -) => { - const { data, error } = await instanceBaaS - .from('story_collaborators') - .delete() - .match({ user_id: userId, story_id: storyId }); - - if (error) throw new Error(error.message); - return data; -}; - -export const getLikedStories = async (userId: number) => { - const { data, error } = await instanceBaaS - .from('story_likes') - .select('story_id') - .eq('user_id', userId); - - if (error) throw new Error(error.message); - return data; -}; - -export const getStoriesByIds = async (storyIds: string[]) => { - if (storyIds.length === 0) return []; - - const { data, error } = await instanceBaaS - .from('Stories') - .select('*') - .in('story_id', storyIds) - .order('created_at', { ascending: false }); // 필요시 정렬 - - if (error) throw new Error(error.message); - return data; -}; - -export const getLikedStoryList = async (userId: number) => { - const liked = await getLikedStories(userId); - - const storyIds = liked.map((item) => item.story_id); - const stories = await getStoriesByIds(storyIds); - return stories; -}; - -export const leaveJoinSocial = async ({ id }: LeaveJoinSocialRequest) => { - try { - const response = await instance.delete(API_PATH.SOCIAL + `/${id}/leave`); - return response.data; - } catch (error) { - throw error; - } -}; - -// TODO: 달램 api 모임장의 모임 삭제 기능 (필요 없을시 지워도됨) -export const cancelJoinSocial = async ({ id }: LeaveJoinSocialRequest) => { - try { - const response = await instance.put(API_PATH.SOCIAL + `/${id}/cancel`); - return response.data; - } catch (error) { - throw error; - } -}; diff --git a/src/api/mypage/type.ts b/src/api/mypage/type.ts deleted file mode 100644 index 7d041893..00000000 --- a/src/api/mypage/type.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LocationType, SocialType } from '@/api/social/type'; - -export interface GetJoinedSocialListParams { - completed?: boolean; - reviewed?: boolean; - limit?: number; - offset?: number; - sortBy?: 'dateTime' | 'registrationEnd' | 'joinedAt'; - sortOrder?: 'asc' | 'desc'; -} - -export interface JoinedSocialResponse { - id: number; - type: SocialType | 'DALLAEMFIT'; - name: string; - dateTime: string; - registrationEnd: string; - location: LocationType; - participantCount: number; - capacity: number; - image: string; - createdBy: number; - canceledAt: string | null; - joinedAt: string; - isCompleted: boolean; - isReviewed: boolean; -} - -export interface LeaveJoinSocialRequest { - id: number; -} diff --git a/src/api/story-collaborators/api.ts b/src/api/story-collaborators/api.ts index 693ad8a0..427523c5 100644 --- a/src/api/story-collaborators/api.ts +++ b/src/api/story-collaborators/api.ts @@ -30,11 +30,18 @@ export const createCollaborator = async (params: CreateCollaboratorRequest) => { } }; -export const getStoryCollaborators = async (storyId: string) => { - const { data } = await instanceBaaS - .from(DB_PATH.STORY_COLLABORATORS) - .select('*') - .eq('story_id', storyId); +export const getStoryCollaborators = async ( + storyId: T +) => { + const query = instanceBaaS.from(DB_PATH.STORY_COLLABORATORS).select('*'); + + if (Array.isArray(storyId)) { + query.in('story_id', storyId); + } else { + query.eq('story_id', storyId); + } + + const { data } = await query; return data; }; diff --git a/src/app/mypage/_components/my-profile/EditMyProfileForm.tsx b/src/app/mypage/_components/my-profile/EditMyProfileForm.tsx index 93091f98..d4f4de5a 100644 --- a/src/app/mypage/_components/my-profile/EditMyProfileForm.tsx +++ b/src/app/mypage/_components/my-profile/EditMyProfileForm.tsx @@ -1,5 +1,3 @@ -'use client'; - import Button from '@/components/common/Button/Button'; import { MyInfoRequest } from '@/api/auth/type'; import { diff --git a/src/app/mypage/_components/my-profile/InputController.tsx b/src/app/mypage/_components/my-profile/InputController.tsx index 77b26950..238abcd0 100644 --- a/src/app/mypage/_components/my-profile/InputController.tsx +++ b/src/app/mypage/_components/my-profile/InputController.tsx @@ -1,5 +1,3 @@ -'use client'; - import InputForm from '@/components/common/Form/InputForm'; import { Control, Controller, FieldValues, Path } from 'react-hook-form'; diff --git a/src/app/mypage/_components/my-profile/MyProfile.tsx b/src/app/mypage/_components/my-profile/MyProfile.tsx index 80901433..67222ed5 100644 --- a/src/app/mypage/_components/my-profile/MyProfile.tsx +++ b/src/app/mypage/_components/my-profile/MyProfile.tsx @@ -3,9 +3,11 @@ import Image from 'next/image'; import useBoolean from '@/hooks/useBoolean'; import { BtnEditLarge, DefaultProfileImage } from '@public/assets/icons'; -import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; import EditMyProfileForm from './EditMyProfileForm'; import MyProfileSkeleton from './MyProfileSkeleton'; +import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; +import { redirect } from 'next/navigation'; +import { APP_ROUTES } from '@/constants/appRoutes'; const MyProfile = () => { const { @@ -15,8 +17,8 @@ const MyProfile = () => { } = useBoolean(); const { myInfo, isSignIn, queryMethods } = useAuth(); + if (!isSignIn || !myInfo) return redirect(APP_ROUTES.signin); - if (!isSignIn || !myInfo) return; if (queryMethods.isLoading) return ; return ( diff --git a/src/app/mypage/_components/my-social-list/MySocialList.tsx b/src/app/mypage/_components/my-social-list/MySocialList.tsx index bbde5832..2efa2493 100644 --- a/src/app/mypage/_components/my-social-list/MySocialList.tsx +++ b/src/app/mypage/_components/my-social-list/MySocialList.tsx @@ -4,7 +4,7 @@ import Observer from '@/components/common/Observer/Observer'; import { useEffect, useState } from 'react'; import { useMySocialList } from '@/hooks/api/mypage/useMySocialList'; import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; -import { LikedStoryItem, SocialItem, TabType } from './type'; +import { LikedStoryResponse, MySocialResponse, TabType } from './type'; import TabMenu from './TabMenu'; import MySocialListCard from './MySocialListCard'; @@ -14,7 +14,7 @@ const MySocialList = () => { const userId = myInfo?.id; const { data, fetchNextPage, hasNextPage, isFetching, isLoading, refetch } = - useMySocialList(activeTab, userId); + useMySocialList(activeTab, userId as number); useEffect(() => { if (data) { @@ -27,14 +27,13 @@ const MySocialList = () => { }; const flattenedList = data?.pages.flat() || []; - const filteredList = flattenedList; return (
- {!isLoading && filteredList.length === 0 && ( + {!isLoading && flattenedList.length === 0 && (

{activeTab === 'joined' ? '내가 참여한 모임이 아직 없어요' @@ -44,12 +43,12 @@ const MySocialList = () => {

)} - {!isLoading && filteredList.length > 0 && ( + {!isLoading && flattenedList.length > 0 && ( { return ( <> - {list.map((item, index) => - activeTab === 'liked' ? ( - - ) : ( - - ) - )} + {list.map((item, index) => ( + + ))} ); }; diff --git a/src/app/mypage/_components/my-social-list/MySocialListCardItem.tsx b/src/app/mypage/_components/my-social-list/MySocialListCardItem.tsx new file mode 100644 index 00000000..917d2229 --- /dev/null +++ b/src/app/mypage/_components/my-social-list/MySocialListCardItem.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { + LikedStoryResponse, + MySocialListCardItemProps, + MySocialResponse, +} from './type'; +import ListCard from '@/components/common/Card/ListCard'; +import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; +import { useRouter } from 'next/navigation'; +import getSocialActionMessage from '@/utils/getSocialActionMessage'; +import { APP_ROUTES } from '@/constants/appRoutes'; +import { Heart } from 'lucide-react'; +import useLikeStory from '@/hooks/api/library/useLikeStory'; +import toast from '@/utils/toast'; +import { deleteCollaboratorFromSocial } from '@/lib/supabase/repositories/story_collaborators'; + +const MySocialListCardItem = ({ + item, + activeTab, + refetch, +}: MySocialListCardItemProps) => { + const router = useRouter(); + const { myInfo } = useAuth(); + const userId = myInfo?.id ?? 0; + + const isJoinedTab = activeTab === 'joined'; + const isLikedTab = activeTab === 'liked'; + + const messages = { + confirm: getSocialActionMessage('모임').confirm('exit'), + success: getSocialActionMessage('모임').success('exit'), + }; + + const { handleLikeStory, isLiked, isPending } = useLikeStory({ + story_id: item.story_id, + user_id: userId, + }); + + const handleClickLike = () => { + if (isPending) return; + handleLikeStory(); + }; + + if (!myInfo || !userId) { + toast.error('사용자 정보를 확인할 수 없습니다.'); + router.push(APP_ROUTES.signin); + return null; + } + + const handleExitSocial = async (storyId: string) => { + const confirmed = window.confirm(messages.confirm); + if (!confirmed) return; + await deleteCollaboratorFromSocial(userId, storyId); + toast.success(messages.success); + refetch(); + }; + + const handleSocialAction = (storyId: string) => { + if (isJoinedTab) handleExitSocial(storyId); + else router.push(`${APP_ROUTES.libraryDetail}/${storyId}/?page=0`); + }; + + return ( +
+ handleSocialAction(item.story_id)} + /> + {isLikedTab && ( + + )} +
+ ); +}; + +export default MySocialListCardItem; diff --git a/src/app/mypage/_components/my-social-list/MySocialListCardItemGeneral.tsx b/src/app/mypage/_components/my-social-list/MySocialListCardItemGeneral.tsx deleted file mode 100644 index 10048071..00000000 --- a/src/app/mypage/_components/my-social-list/MySocialListCardItemGeneral.tsx +++ /dev/null @@ -1,93 +0,0 @@ -'use client'; - -import { - deleteCollaboratorFromSocial, - leaveJoinSocial, -} from '@/api/mypage/api'; -import { MySocialListItemProps } from './type'; -import ListCard from '@/components/common/Card/ListCard'; -import useCollaboratorList from '@/hooks/api/mypage/useCollaboratorList'; -import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; -import convertLocationToGenre from '@/utils/convertLocationToGenre'; -import { useRouter } from 'next/navigation'; -import getSocialActionMessage from '@/utils/getSocialActionMessage'; -import { useStoryIdBySocialId } from '@/hooks/api/supabase/useStoryIdBySocialId'; -import { LocationType } from '@/api/social/type'; -import { APP_ROUTES } from '@/constants/appRoutes'; - -const MySocialListCardItemGeneral = ({ - item, - activeTab, - refetch, -}: MySocialListItemProps) => { - const router = useRouter(); - const nowDate = new Date().toISOString(); - const { data: collaborator } = useCollaboratorList(Number(item.id)); - const collaboratorCount = collaborator?.length || 0; - const { myInfo } = useAuth(); - const userId = myInfo?.id; - const isJoined = activeTab === 'joined'; - - const { data: storyId, isLoading: isStoryLoading } = useStoryIdBySocialId( - item.id - ); - - const handleMySocial = async (id: number) => { - if (!storyId) return; - - try { - const messages = { - confirm: getSocialActionMessage('모임').confirm('exit'), - success: getSocialActionMessage('모임').success('exit'), - }; - - if (isJoined) { - const confirmed = window.confirm(messages.confirm); - if (!confirmed) return; - await leaveJoinSocial({ id }); - if (userId) { - await deleteCollaboratorFromSocial(userId, storyId); - alert(messages.success); - refetch(); - } - } else { - router.push(`${APP_ROUTES.socialDetail}/${storyId}/?page=0`); - } - } catch (error) { - console.error(error); - alert('모임 취소에 실패했습니다.'); - } - }; - - return ( -
- handleMySocial(item.id)} - /> -
- ); -}; - -export default MySocialListCardItemGeneral; diff --git a/src/app/mypage/_components/my-social-list/MySocialListItemLiked.tsx b/src/app/mypage/_components/my-social-list/MySocialListItemLiked.tsx deleted file mode 100644 index 0e6af6df..00000000 --- a/src/app/mypage/_components/my-social-list/MySocialListItemLiked.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import ListCard from '@/components/common/Card/ListCard'; -import { MySocialListCardItemLikedProps } from '@/app/mypage/_components/my-social-list/type'; -import useCollaboratorList from '@/hooks/api/mypage/useCollaboratorList'; -import { Heart } from 'lucide-react'; -import useLikeStory from '@/hooks/api/library/useLikeStory'; -import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; -import toast from '@/utils/toast'; -import { APP_ROUTES } from '@/constants/appRoutes'; - -const MySocialListCardItemLiked = ({ - item, -}: MySocialListCardItemLikedProps) => { - const { isSignIn, myInfo } = useAuth(); - const router = useRouter(); - const { data: collaborator } = useCollaboratorList(Number(item.social_id)); - const collaboratorCount = collaborator?.length || 0; - const { handleLikeStory, isLiked, isPending } = useLikeStory({ - story_id: item.story_id as string, - user_id: myInfo?.id as number, - }); - const handleClickLike = () => { - if (isPending) return; - handleLikeStory(); - if (!isSignIn) { - toast({ - message: - '로그인이 필요합니다. 로그인 페이지로 이동합니다.', - type: 'error', - duration: 5, - }); - router.push('/signin'); - } - }; - return ( -
-
- - router.push(`${APP_ROUTES.libraryDetail}/${item.story_id}/?page=0`) - } - /> - -
-
- ); -}; - -export default MySocialListCardItemLiked; diff --git a/src/app/mypage/_components/my-social-list/type.ts b/src/app/mypage/_components/my-social-list/type.ts index 374853c2..ff628dbe 100644 --- a/src/app/mypage/_components/my-social-list/type.ts +++ b/src/app/mypage/_components/my-social-list/type.ts @@ -1,43 +1,42 @@ -export const TAB_TYPES = ['joined', 'created', 'liked'] as const; +import { GenreType } from '@/api/social/type'; +export const TAB_TYPES = ['joined', 'created', 'liked'] as const; export type TabType = (typeof TAB_TYPES)[number]; + export interface TabMenuProps { activeTab: TabType; onTabChange: (tab: TabType) => void; } -export interface SocialItem { - id: number; - name: string; - image: string; - location: string; - registrationEnd: string; +export interface LikedStoryResponse { capacity: number; - canceledAt: string | null; -} - -export interface LikedStoryItem { + collaborator_count: number; + cover_image_url: string; + genre: string; + liked_at: string; story_id: string; - social_id: number; title: string; - genre: string; - cover_image_url: string; - approved_count: number; - capacity?: number; } -export interface MySocialListCardItemLikedProps { - item: LikedStoryItem; +export interface MySocialResponse { + capacity: number; + cover_image_url: string; + genre: GenreType; + title: string; + collaborator_count: number; + joined_at: string; + role: 'LEADER' | 'MEMBER' | 'GUEST'; + story_id: string; } -export interface MySocialListItemProps { - item: SocialItem; +export interface MySocialListCardItemProps { + item: MySocialResponse | LikedStoryResponse; activeTab: TabType; refetch: () => void; } export interface MySocialListCardProps { - list: LikedStoryItem[] | SocialItem[]; + list: (LikedStoryResponse | MySocialResponse)[]; activeTab: TabType; refetch: () => void; } diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 86c27f69..5657e90a 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,15 +1,7 @@ -'use client'; - -import { redirect } from 'next/navigation'; -import { useAuth } from '@/providers/auth-provider/AuthProvider.client'; -import { APP_ROUTES } from '@/constants/appRoutes'; import MyProfile from './_components/my-profile/MyProfile'; import MySocialList from './_components/my-social-list/MySocialList'; const MyPage = () => { - const { isSignIn } = useAuth(); - if (!isSignIn) return redirect(APP_ROUTES.signin); - return (

마이 페이지

diff --git a/src/components/common/Card/ListCard.tsx b/src/components/common/Card/ListCard.tsx index b44514e8..a73097ad 100644 --- a/src/components/common/Card/ListCard.tsx +++ b/src/components/common/Card/ListCard.tsx @@ -15,6 +15,7 @@ const ListCard = ({ chip, textContent, endDate, + endDateTitle, isCardDataLoading, isCompletedStory, isCanceled, @@ -44,7 +45,7 @@ const ListCard = ({ {!isImageLoaded || isImageLoadError || isCardDataLoading ? (
) : null} - {image.src && image.alt && !isCardDataLoading && ( + {image.src && image.alt && !isCardDataLoading ? ( {image.alt + ) : ( +
+ No Image +
)}
@@ -87,7 +92,11 @@ const ListCard = ({
{!isCardDataLoading ? ( <> - {endDate &&

종료 : {format(endDate, 'yyyy-MM-dd')}

} + {endDate && ( +

+ {endDateTitle} : {format(endDate, 'yyyy-MM-dd')} +

+ )}