diff --git a/src/_queries/gathering/gathering-detail-queries.ts b/src/_queries/gathering/gathering-detail-queries.ts index e7e29cc5..3a1c888a 100644 --- a/src/_queries/gathering/gathering-detail-queries.ts +++ b/src/_queries/gathering/gathering-detail-queries.ts @@ -3,10 +3,15 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { GetGatheringDetail, createGathering } from '@/src/_apis/gathering/gathering-detail-apis'; import { CreateGatheringRequestTypes } from '@/src/types/gathering-data'; -export function useGetGatheringDetailQuery(crewId: number, gatheringId: number) { +export function useGetGatheringDetailQuery( + crewId: number, + gatheringId: number, + options?: { enabled?: boolean }, +) { return useQuery({ queryKey: ['gatheringDetail', crewId, gatheringId], queryFn: () => GetGatheringDetail(crewId, gatheringId), + ...options, }); } diff --git a/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx b/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx index d7a85b08..2fb2daf0 100644 --- a/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx +++ b/src/app/(crew)/crew/create/_components/create-crew-form/index.tsx @@ -287,13 +287,13 @@ export default function CreateCrewForm({ htmlFor="crew-totalCount" className="text-base font-semibold text-gray-800 md:text-xl" > - 모집 정원을 선택해주세요. + 크루 최대 인원을 선택해주세요. ( { if (isAuth) { open(); // 로그인 상태일 경우 모달 열기 @@ -30,7 +38,13 @@ export default function CreateGathering({ crewId }: { crewId: number }) { }; return ( - <> +
+
+

약속 잡기

+ + 현재 {totalGatheringCount}개의 약속이 개설되어 있습니다. + +
@@ -40,6 +54,6 @@ export default function CreateGathering({ crewId }: { crewId: number }) { close={close} data={initialValue} /> - +
); } diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx index 41cf87a9..bd87cae7 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useRouter } from 'next/navigation'; +import { Loader } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { cancelCrew, joinCrew, leaveCrew } from '@/src/_apis/crew/crew-detail-apis'; import { useUser } from '@/src/_queries/auth/user-queries'; @@ -20,6 +21,7 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { const [isCaptain, setIsCaptain] = useState(false); const [isMember, setIsMember] = useState(false); const [isJoining, setIsJoining] = useState(false); + const [isConfirmed, setIsConfirmed] = useState(false); const [confirmCancelOpened, { open: openConfirmCancel, close: closeConfirmCancel }] = useDisclosure(); const router = useRouter(); @@ -35,12 +37,20 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { const { data, isLoading, error: fetchError, refetch } = useGetCrewDetailQuery(id); useEffect(() => { - if (currentUserId && data) { - const captain = data.crewMembers.find((member) => member.captain); - const memberExists = data.crewMembers.some((member) => member.id === currentUserId); + if (data) { + // confirmed 상태 계산 + if (data.participantCount !== undefined && data.totalCount !== undefined) { + setIsConfirmed(data.participantCount === data.totalCount); + } + + // Captain 및 멤버 여부 확인 (currentUserId 필요) + if (currentUserId) { + const captain = data.crewMembers.find((member) => member.captain); + const memberExists = data.crewMembers.some((member) => member.id === currentUserId); - setIsCaptain(captain?.id === currentUserId); - setIsMember(memberExists); + setIsCaptain(captain?.id === currentUserId); + setIsMember(memberExists); + } } }, [currentUserId, data]); @@ -88,7 +98,7 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { toast.success('크루가 성공적으로 삭제되었습니다.'); router.push('/'); } catch (deleteError) { - toast.error('크루 삭제 중 에러가 발생했습니다.'); + toast.error('🚫 크루 삭제 중 에러가 발생했습니다.'); } }; @@ -106,9 +116,10 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { // TODO: 로딩, 에러처리 추후 개선 if (isLoading) { - return

Loading...

; + return ; } + // TODO: 추후 404페이지로 이동시키기 if (fetchError) { if (fetchError instanceof ApiError) { try { @@ -118,7 +129,7 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { return

크루 정보를 찾을 수 없습니다

; } } catch (parseError) { - return

{`Error ${fetchError.status}: ${fetchError.message}`}

; + return

{`Error ${fetchError.message}`}

; } } return

데이터 통신에 실패했습니다.

; @@ -135,6 +146,7 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { isCaptain={isCaptain} isMember={isMember} isJoining={isJoining} + isConfirmed={isConfirmed} handleJoinClick={handleJoinClick} handleLeaveCrew={handleLeaveCrew} handleDelete={handleDelete} diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx index 8c9d0ebf..d6d4cf32 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx @@ -16,6 +16,7 @@ interface DetailCrewPresenterProps { isCaptain: boolean; isMember: boolean; isJoining: boolean; + isConfirmed: boolean; handleJoinClick: () => void; handleLeaveCrew: () => void; handleDelete: () => void; @@ -26,6 +27,7 @@ export default function DetailCrewPresenter({ data, isCaptain, isMember, + isConfirmed, handleJoinClick, handleLeaveCrew, handleDelete, @@ -42,7 +44,6 @@ export default function DetailCrewPresenter({ totalCount, imageUrl, crewMembers, - confirmed, } = data; // captain과 members 분리 @@ -50,6 +51,7 @@ export default function DetailCrewPresenter({ const members = crewMembers.filter((member) => !member.captain); const getJoinButtonText = () => { + if (isConfirmed) return '모집 완료'; if (isMember) return '참여 완료'; if (isJoining) return '참여 중...'; return '크루 참여하기'; @@ -71,23 +73,23 @@ export default function DetailCrewPresenter({ {/* 오른쪽 하단 공유 및 케밥 버튼 */} -
+
{isCaptain || isMember ? ( -
+
+
{isCaptain ? ( @@ -151,19 +153,20 @@ export default function DetailCrewPresenter({
유저 아이콘 - 참여인원 + 크루멤버 {participantCount} /{totalCount}
- {confirmed && ( + {isConfirmed && (
- 개설 확정 - 개설확정 + 모집 완료 아이콘 + 모집 완료
)}
+

크루장을 제외한 멤버 목록입니다.

{members.length > 0 ? ( diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx index b1c84bfe..e9e3bb50 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx @@ -33,7 +33,6 @@ export const Default: Story = { subLocation: '유성구', participantCount: 8, totalCount: 10, - confirmed: true, imageUrl: 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', totalGatheringCount: 5, @@ -117,7 +116,6 @@ export const IsCaptain: Story = { subLocation: '유성구', participantCount: 1, totalCount: 10, - confirmed: true, imageUrl: 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', totalGatheringCount: 1, @@ -149,7 +147,6 @@ export const IsMember: Story = { subLocation: '유성구', participantCount: 2, totalCount: 10, - confirmed: true, imageUrl: 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', totalGatheringCount: 1, diff --git a/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx index 1851a336..37a61136 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx @@ -14,14 +14,14 @@ export interface GatheringDetailModalContainerProps { opened: boolean; close: () => void; data: GatheringDetailType; + onUpdate?: () => void; } -// NOTE: 테스트는 로그인 후 토큰이 안담겨서 추후 진행하겠습니다! - export default function GatheringDetailModalContainer({ opened, close, data, + onUpdate, }: GatheringDetailModalContainerProps) { const showToast = (message: string, type: 'success' | 'error' | 'warning') => { toast(message, { type }); @@ -32,6 +32,7 @@ export default function GatheringDetailModalContainer({ await JoinGathering(data.crewId, data.id); showToast('약속에 참여했습니다.', 'success'); close(); + onUpdate?.(); } catch (error) { if (error instanceof ApiError) { showToast(`참여 중 에러 발생: ${error.message}`, 'error'); @@ -43,6 +44,7 @@ export default function GatheringDetailModalContainer({ try { await LeaveGathering(data.crewId, data.id); close(); + onUpdate?.(); } catch (error) { if (error instanceof ApiError) { showToast(`참여 취소 중 에러 발생: ${error.message}`, 'error'); @@ -55,6 +57,7 @@ export default function GatheringDetailModalContainer({ await CancelGathering(data.crewId, data.id); showToast('약속을 삭제했습니다.', 'success'); close(); + onUpdate?.(); } catch (error) { if (error instanceof ApiError) { showToast(`약속 삭제 중 에러 발생: ${error.message}`, 'error'); diff --git a/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx index 17a55a50..b827d938 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/gathering-detail-modal.stories.tsx @@ -43,10 +43,19 @@ const Template: StoryFn = function Gathering setIsOpened(opened); }, [opened]); + const handleUpdate = () => { + action('Update action performed')(); + }; + return ( <> - + ); }; 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 ed62d4c9..72d638bc 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 @@ -3,6 +3,7 @@ import { useState } from 'react'; import { toast } from 'react-toastify'; import { useRouter } from 'next/navigation'; +import { Loader } from '@mantine/core'; import { addLike, removeLike } from '@/src/_apis/liked/liked-apis'; import { useGetGatheringListQuery } from '@/src/_queries/crew/gathering-list-queries'; import { ApiError } from '@/src/utils/api'; @@ -14,7 +15,7 @@ interface GatheringListSectionProps { } export default function GatheringListSection({ id }: GatheringListSectionProps) { - const { data: gatheringList, isLoading, error } = useGetGatheringListQuery(id); + const { data: gatheringList, isLoading, error, refetch } = useGetGatheringListQuery(id); const [showLoginModal, setShowLoginModal] = useState(false); const router = useRouter(); @@ -43,11 +44,15 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) router.push(`/login?redirect=${encodeURIComponent(currentPath)}`); }; + const handleModalAction = () => { + refetch(); + }; + // TODO: 추후 에러, 로딩 수정 if (isLoading) return (
-

로딩 중...

+
); @@ -73,6 +78,7 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) onLike={handleLike} onUnlike={handleUnlike} onShowLoginModal={() => setShowLoginModal(true)} + onModalAction={handleModalAction} /> {showLoginModal && (
-
-

크루 약속

- -
+
diff --git a/src/components/common/crew-list/crew-card-list.tsx b/src/components/common/crew-list/crew-card-list.tsx index c6482646..50594e7b 100644 --- a/src/components/common/crew-list/crew-card-list.tsx +++ b/src/components/common/crew-list/crew-card-list.tsx @@ -34,35 +34,44 @@ export default function CrewCardList({ data, inWhere }: CrewCardListProps) { return (
    - {crewDataList.map((inform) => ( -
  • - -
  • - ))} + {crewDataList.map((inform) => { + // MainCrewList에서만 isConfirmed 계산 + const isConfirmed = + inWhere !== 'my-crew' && + (inform as MainCrewList).participantCount !== undefined && + (inform as MainCrewList).totalCount !== undefined && + (inform as MainCrewList).participantCount === (inform as MainCrewList).totalCount; + + return ( +
  • + +
  • + ); + })}
); } diff --git a/src/components/common/crew-list/crew-card.tsx b/src/components/common/crew-list/crew-card.tsx index 4da7d52b..7ee97ac6 100644 --- a/src/components/common/crew-list/crew-card.tsx +++ b/src/components/common/crew-list/crew-card.tsx @@ -83,7 +83,7 @@ export default function CrewCard({ {isConfirmed && ( - 개설 확정 + 모집 완료 )}
diff --git a/src/components/common/gathering-card/container.tsx b/src/components/common/gathering-card/container.tsx index 29d296b3..2a4de2da 100644 --- a/src/components/common/gathering-card/container.tsx +++ b/src/components/common/gathering-card/container.tsx @@ -6,7 +6,7 @@ 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/detail/[id]/_components/gathering-detail-modal/container'; -import { GatheringType } from '@/src/types/gathering-data'; +import { GatheringData, GatheringDetailType, GatheringType } from '@/src/types/gathering-data'; import GatheringCardPresenter from './presenter'; interface GatheringCardContainerProps extends GatheringType { @@ -14,6 +14,7 @@ interface GatheringCardContainerProps extends GatheringType { crewId: number; onLike: (gatheringId: number) => Promise; onUnlike: (gatheringId: number) => Promise; + onModalAction: () => void; } export default function GatheringCard({ @@ -29,11 +30,10 @@ export default function GatheringCard({ crewId, onLike, onUnlike, + onModalAction, }: GatheringCardContainerProps) { const [opened, { open, close }] = useDisclosure(false); const [isLiked, setIsLiked] = useState(initialIsLiked); - - // 날짜 비교 const gatheringDate = new Date(dateTime); const today = new Date(); const isPast = gatheringDate < today; @@ -45,6 +45,21 @@ export default function GatheringCard({ // 마감 시간 문자열 생성 const deadlineMessage = `오늘 ${gatheringDate.getHours()}시 마감`; + // API 데이터 가져오기 (모달이 열릴 때만 호출) + const { + data: gatheringData, + error, + refetch, + } = useGetGatheringDetailQuery(crewId, id, { + enabled: false, // 초기에는 비활성화 + }); + + // 모달 열기 + const openModal = () => { + refetch(); // 모달 열릴 때 데이터 가져오기 + open(); + }; + // 찜하기 상태 업데이트 const handleLikeToggle = async () => { try { @@ -55,35 +70,18 @@ export default function GatheringCard({ await onLike(id); setIsLiked(true); } - } catch (error) { + } catch (likeError) { toast.error('찜 상태를 업데이트하는 데 실패했습니다.'); } }; - const { data: gatheringData, error } = useGetGatheringDetailQuery(crewId, id); - + // 에러 처리 useEffect(() => { if (error) { - if (error instanceof ApiError) { - try { - const errorData = JSON.parse(error.message); - - if (errorData.status === 'NOT_FOUND') { - toast.error('모임 정보를 찾을 수 없습니다.'); - } - } catch { - toast.error(`Error ${error.status}: ${error.message}`); - } - } else { - toast.error('데이터 통신에 실패했습니다.'); - } + toast.error('데이터를 가져오는 데 실패했습니다.'); } }, [error]); - const openModal = () => { - open(); - }; - return ( <> {opened && gatheringData && ( - + )} ); diff --git a/src/components/common/gathering-card/presenter.tsx b/src/components/common/gathering-card/presenter.tsx index 40b315e3..25bc54fa 100644 --- a/src/components/common/gathering-card/presenter.tsx +++ b/src/components/common/gathering-card/presenter.tsx @@ -52,8 +52,7 @@ export default function GatheringCardPresenter({
@@ -73,7 +72,7 @@ export default function GatheringCardPresenter({
)} -
+

@@ -84,14 +83,14 @@ export default function GatheringCardPresenter({ {time}

-

+

{title}

{location}

-
+
person icon

@@ -104,7 +103,7 @@ export default function GatheringCardPresenter({

-
+
diff --git a/src/components/gathering-list/crew-gathering-list.tsx b/src/components/gathering-list/crew-gathering-list.tsx index 53da9124..999aa30c 100644 --- a/src/components/gathering-list/crew-gathering-list.tsx +++ b/src/components/gathering-list/crew-gathering-list.tsx @@ -14,6 +14,7 @@ interface CrewGatheringListProps { onLike: (gatheringId: number) => Promise; onUnlike: (gatheringId: number) => Promise; onShowLoginModal: () => void; + onModalAction: () => void; } export default function CrewGatheringList({ @@ -22,6 +23,7 @@ export default function CrewGatheringList({ onLike, onUnlike, onShowLoginModal, + onModalAction, }: CrewGatheringListProps) { const { sliderRef, handleMouseDown, handleMouseLeave, handleMouseUp, handleMouseMove } = useSlider(); @@ -102,6 +104,7 @@ export default function CrewGatheringList({ className="w-full" onLike={() => handleLikeAction('like', card.id)} onUnlike={() => handleLikeAction('unlike', card.id)} + onModalAction={onModalAction} />
))} @@ -152,6 +155,7 @@ export default function CrewGatheringList({ className="w-full" onLike={() => handleLikeAction('like', card.id)} onUnlike={() => handleLikeAction('unlike', card.id)} + onModalAction={onModalAction} /> ))} diff --git a/src/components/gathering-list/liked-list-container.tsx b/src/components/gathering-list/liked-list-container.tsx index 6cb1a3b9..d20a307c 100644 --- a/src/components/gathering-list/liked-list-container.tsx +++ b/src/components/gathering-list/liked-list-container.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { toast } from 'react-toastify'; +import { Loader } from '@mantine/core'; import { addLike, removeLike } from '@/src/_apis/liked/liked-apis'; import { useGetLikedListQuery } from '@/src/_queries/liked/liked-queries'; import { ApiError } from '@/src/utils/api'; diff --git a/src/components/gathering-list/liked-list-presenter.tsx b/src/components/gathering-list/liked-list-presenter.tsx index 0a643629..8f647132 100644 --- a/src/components/gathering-list/liked-list-presenter.tsx +++ b/src/components/gathering-list/liked-list-presenter.tsx @@ -26,7 +26,7 @@ export default function LikedListPresenter({ className={cn( 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3', 'mx-auto place-content-start justify-items-center gap-4', - 'md:min-h-[1064px] lg:min-h-[704px]', + 'md:min-h-[1172px] lg:min-h-[776px]', )} > {content.map((card) => ( diff --git a/src/types/crew-card.d.ts b/src/types/crew-card.d.ts index 4f93e28a..74e12835 100644 --- a/src/types/crew-card.d.ts +++ b/src/types/crew-card.d.ts @@ -59,7 +59,6 @@ export interface CrewDetail { imageUrl: string; totalGatheringCount: number; crewMembers: CrewDetailMember[]; - confirmed: boolean; introduce?: string; }