Skip to content
18 changes: 11 additions & 7 deletions src/app/(user-page)/my-meeting/_features/Participated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IMyMeetingParticipated } from 'types/myMeeting';

import CardRightSection from './CardRightSection';
import LeaveMeetingButton from './LeaveMeetingButton';
import PendingSection from './PendingSection';
import PendingStatusChip from './PendingStatusChip';
import MeetingListSkeleton from './skeletons/SkeletonMeetingList';

Expand Down Expand Up @@ -83,6 +84,9 @@ const Participated = () => {

return (
<div>
{/* 승인 대기중인 모임 섹션 (상단에 배치) */}
<PendingSection />
<h1 className="typo-head1 text-white">나의 Deving 모임</h1>
{meetingData.pages.map((page, pageIdx) => (
<div key={pageIdx}>
{page.content.map((meeting) => (
Expand Down Expand Up @@ -193,11 +197,6 @@ const Participated = () => {
className="flex lg:hidden"
meetingId={meeting.meetingId}
/>

{meeting.myMemberStatus === 'PENDING' && (
<PendingStatusChip meetingId={meeting.meetingId} />
)}

{meeting.myMemberStatus === 'APPROVED' &&
!meeting.isMeetingManager && (
<LeaveMeetingButton
Expand All @@ -216,8 +215,13 @@ const Participated = () => {
))}

{/* 무한 스크롤을 위한 별도의 Observer 요소 */}
{hasNextPage && <div ref={lastMeetingRef} id="infinite-scroll-trigger" />}

{hasNextPage && (
<div
ref={lastMeetingRef}
id="infinite-scroll-trigger"
className="mt-10 h-10" // 높이와 마진 추가
/>
)}
{/* 추가 데이터 로딩 중 표시 */}
{isFetchingNextPage && <MeetingListSkeleton />}

Expand Down
134 changes: 134 additions & 0 deletions src/app/(user-page)/my-meeting/_features/PendingSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import HorizonCard from '@/components/ui/HorizonCard';
import { useInfiniteMyMeetingPendingQueries } from '@/hooks/queries/useMyMeetingQueries';
import { translateCategoryNameToEng } from '@/util/searchFilter';
import { ChevronRight } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { IMyMeetingPending } from 'types/myMeeting';

import PendingStatusChip from './PendingStatusChip';
import MeetingListSkeleton from './skeletons/SkeletonMeetingList';

const PendingSection = () => {
const router = useRouter();
const [visiblePages, setVisiblePages] = useState(1);

const {
data: pendingData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteMyMeetingPendingQueries();

const getMeetingDetailUrl = (meeting: IMyMeetingPending) =>
`/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`;

const handleCardClick = (meeting: IMyMeetingPending) => {
router.push(getMeetingDetailUrl(meeting));
};

// 더 보기 버튼 클릭 핸들러
const handleLoadMore = () => {
if (hasNextPage) {
fetchNextPage().then(() => {
setVisiblePages((prev) => prev + 1);
});
}
};

if (isLoading) {
return <MeetingListSkeleton />;
}

if (!pendingData || pendingData.pages[0].content.length === 0) {
return null;
}

// 현재 표시할 페이지만 필터링
const visibleData = pendingData.pages.slice(0, visiblePages);

return (
<div className="mb-12 mt-12">
{/* 스크롤 컨테이너 */}
<div
className="custom-scrollbar flex overflow-x-auto pb-4"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#3853EA #30333E',
}}
>
<style jsx>{`
.custom-scrollbar::-webkit-scrollbar {
height: 62px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #30333e;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #3853ea;
border-radius: 4px;
}
`}</style>
{visibleData.map((page, pageIdx) => (
<div key={pageIdx} className="flex space-x-4">
{page.content.map((meeting) => (
<div
key={meeting.meetingId}
className="relative min-w-[320px] lg:min-w-[500px]"
>
<HorizonCard
onClick={() => handleCardClick(meeting)}
title={meeting.title}
thumbnailUrl={meeting.thumbnail}
location={meeting.location}
total={meeting.maxMember}
value={meeting.memberCount}
className="flex-row"
meetingId={meeting.meetingId}
showLikeButton={false}
category={''}
thumbnailWidth={160}
thumbnailHeight={160}
></HorizonCard>
<PendingStatusChip meetingId={meeting.meetingId} />
</div>
))}
</div>
))}

{/* 더 보기 버튼 */}
{hasNextPage && (
<div className="ml-2 flex items-center justify-center">
<button
onClick={handleLoadMore}
disabled={isFetchingNextPage}
className={`flex min-h-[100px] min-w-[44px] items-center justify-center rounded-lg border border-Cgray300 bg-main p-2 transition-colors ${
isFetchingNextPage
? 'cursor-not-allowed opacity-70'
: 'hover:bg-Cgray100'
}`}
aria-label="더 많은 대기중인 모임 보기"
>
{isFetchingNextPage ? (
<span className="animate-pulse text-white">로딩중...</span>
) : (
<ChevronRight className="text-white" />
)}
</button>
</div>
)}
</div>

{/* 추가 데이터 로딩 중 표시 (버튼 외에 추가적인 로딩 표시가 필요한 경우) */}
{isFetchingNextPage && !hasNextPage && (
<div className="mt-4">
<MeetingListSkeleton />
</div>
)}
</div>
);
};

export default PendingSection;
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ export const PendingStatusChip = ({
<div className="absolute left-2 top-2 z-10 flex h-8 overflow-hidden rounded-md shadow-md">
<div className="flex items-center bg-main bg-opacity-90 px-3 py-1 text-sm font-medium text-white">
<span>승인 대기중</span>
<span className="ml-1 inline-flex">
<span className="animate-delay-100">.</span>
<span className="animate-delay-200">.</span>
<span className="animate-delay-300">.</span>
</span>
</div>

<button
Expand Down
30 changes: 22 additions & 8 deletions src/app/(user-page)/my-meeting/my/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NotYet from '@/components/common/NotYet';

Check warning on line 1 in src/app/(user-page)/my-meeting/my/page.tsx

View workflow job for this annotation

GitHub Actions / check

'NotYet' is defined but never used. Allowed unused vars must match /^_/u
import { myMeetingKeys } from '@/hooks/queries/useMyMeetingQueries';
import { QUERY_KEYS } from '@/hooks/queries/useMyPageQueries';
import {
Expand All @@ -10,10 +10,15 @@
import {
getMyMeetingManage,
getMyMeetingParticipated,
getMyMeetingPending,
} from 'service/api/mymeeting';
import { getBanner } from 'service/api/mypageProfile';
import { Paginated } from 'types/meeting';
import { IMyMeetingManage, IMyMeetingParticipated } from 'types/myMeeting';
import {
IMyMeetingManage,
IMyMeetingParticipated,
IMyMeetingPending,
} from 'types/myMeeting';

import Created from '../_features/Created';
import Participated from '../_features/Participated';
Expand Down Expand Up @@ -45,13 +50,22 @@
});
} else {
// 내가 참여하고있는 모임 prefetch
await queryClient.prefetchInfiniteQuery({
queryKey: myMeetingKeys.participated(),
queryFn: ({ pageParam }) => getMyMeetingParticipated(pageParam),
getNextPageParam: (lastPage: Paginated<IMyMeetingParticipated>) =>
lastPage.nextCursor ?? false,
initialPageParam: 0,
});
await Promise.all([
queryClient.prefetchInfiniteQuery({
queryKey: myMeetingKeys.participated(),
queryFn: ({ pageParam }) => getMyMeetingParticipated(pageParam),
getNextPageParam: (lastPage: Paginated<IMyMeetingParticipated>) =>
lastPage.nextCursor ?? false,
initialPageParam: 0,
}),
queryClient.prefetchInfiniteQuery({
queryKey: myMeetingKeys.pending(),
queryFn: ({ pageParam }) => getMyMeetingPending(pageParam),
getNextPageParam: (lastPage: Paginated<IMyMeetingPending>) =>
lastPage.nextCursor ?? false,
initialPageParam: 0,
}),
]);
}

return (
Expand Down
23 changes: 21 additions & 2 deletions src/app/meeting/_features/form/MeetingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import {
TitleField,
} from '@/app/meeting/_features/form/form-filed';
import { useToast } from '@/components/common/ToastContext';
import Modal from '@/components/ui/modal/Modal';
import useMeetingFormMutation from '@/hooks/mutations/useMeetingFormMutation';
import { convertImageToBase64 } from '@/util/base64';
import { AxiosError } from 'axios';
import { MEETING_TYPES } from 'constants/category/category';
import { useRouter } from 'next/navigation';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { CreateMeetingPayload, UpdateMeetingPayload } from 'types/meetingForm';

Expand All @@ -37,6 +38,7 @@ export default function MeetingForm({
}: MeetingFormProps) {
const router = useRouter();
const { showToast } = useToast();
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);

// 날짜 YYYY-MM-DD 형식으로 변환
const today = new Date();
Expand All @@ -48,6 +50,10 @@ export default function MeetingForm({
return category ? category.id : '';
};

const handleLogin = () => {
router.push('/login');
};

const { createMeeting, updateMeeting, isLoading } = useMeetingFormMutation({
onSuccessCallback: (response, formData) => {
const isCreateMode = mode === 'create';
Expand All @@ -66,7 +72,10 @@ export default function MeetingForm({
onErrorCallback: (error: AxiosError) => {
let message;

if (mode !== 'create' && error?.response?.status === 403) {
if (mode === 'create' && error?.response?.status === 403) {
setIsLoginModalOpen(true);
return;
} else if (mode !== 'create' && error?.response?.status === 403) {
message = '모임 수정 권한이 없습니다';
} else {
message =
Expand Down Expand Up @@ -155,6 +164,16 @@ export default function MeetingForm({

return (
<FormProvider {...methods}>
<Modal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
onConfirm={handleLogin}
confirmText="로그인"
cancelText="취소"
modalClassName="w-96"
>
<p className="text-center text-white">로그인이 필요한 서비스 입니다.</p>
</Modal>
<div className="mx-auto w-full max-w-3xl p-6">
<h1 className="typo-heading1 mb-8 text-center">
{mode === 'create' ? '모임 생성하기' : '모임 수정하기'}
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/mutations/useMyMeetingMutation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useToast } from '@/components/common/ToastContext';
import axiosInstance from '@/lib/axios/axiosInstance';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { putExpel, putIsPublic, putMemberStatus } from 'service/api/mymeeting';
Expand Down Expand Up @@ -52,7 +51,7 @@ const useCancelPendingMutation = () => {
onSuccess: (_, meetingId) => {
showToast('승인 대기를 취소했습니다.', 'success');
queryClient.invalidateQueries({
queryKey: myMeetingKeys.participated(),
queryKey: myMeetingKeys.pending(),
});
queryClient.invalidateQueries({
queryKey: meetingKeys.detailInfo(meetingId),
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/queries/useMyMeetingQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import {
getMyMeetingManage,
getMyMeetingMemberProfile,
getMyMeetingPending,
} from 'service/api/mymeeting';
import {
getMyMeetingLikes,
Expand All @@ -13,6 +14,7 @@ import {
IMyMeetingManage,
IMyMeetingParticipated,
} from 'types/myMeeting';
import { IMyMeetingPending } from 'types/myMeeting';

export const myMeetingKeys = {
all: ['mymeeting'] as const,
Expand All @@ -24,6 +26,7 @@ export const myMeetingKeys = {
'profile',
{ meetingId, userId },
],
pending: () => [...myMeetingKeys.all, 'pending'] as const,
};

// 내가 생성한 모임
Expand All @@ -50,6 +53,20 @@ export const useInfiniteMyMeetingParticipatedQueries = () => {
});
};

// 대기중인 모임
export const useInfiniteMyMeetingPendingQueries = () => {
return useInfiniteQuery({
queryKey: myMeetingKeys.pending(),
queryFn: ({ pageParam }) => getMyMeetingPending(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage: Paginated<IMyMeetingPending>) => {
return lastPage.nextCursor ?? null;
},
staleTime: 0,
gcTime: 0,
});
};

// 내가 찜한 모임
export const useInfiniteMyMeetingLikesQueries = () => {
return useInfiniteQuery({
Expand Down
1 change: 1 addition & 0 deletions src/service/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const myMeetingURL = {
manage: `${CURRENT_API_VERSION}/mymeetings/manage`,
likes: `${CURRENT_API_VERSION}/mymeetings/likes`,
all: `${CURRENT_API_VERSION}/mymeetings/all`,
pending: `${CURRENT_API_VERSION}/mymeetings/pending`,
quit: (meetingId: number) =>
`${CURRENT_API_VERSION}/mymeetings/quit/${meetingId}`,
cancel: (meetingId: number) =>
Expand Down
Loading
Loading