diff --git a/package-lock.json b/package-lock.json index 2923c63c..24395f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14977,6 +14977,126 @@ "optional": true } } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", + "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", + "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", + "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", + "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", + "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", + "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", + "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", + "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } 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 bd87cae7..e7e2b577 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,13 +3,14 @@ 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'; import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; import { ApiError } from '@/src/utils/api'; +import Button from '@/src/components/common/input/button'; import ConfirmCancelModal from '@/src/components/common/modal/confirm-cancel-modal'; +import CrewDetailSkeleton from '@/src/components/common/skeleton/crew-detail-skeleton'; import { User } from '@/src/types/auth'; import DetailCrewPresenter from './detail-crew-presenter'; @@ -38,12 +39,8 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { useEffect(() => { if (data) { - // confirmed 상태 계산 - if (data.participantCount !== undefined && data.totalCount !== undefined) { - setIsConfirmed(data.participantCount === data.totalCount); - } + 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); @@ -114,29 +111,35 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { }); }; - // TODO: 로딩, 에러처리 추후 개선 if (isLoading) { - return ; + return ; } - // TODO: 추후 404페이지로 이동시키기 - if (fetchError) { + const renderErrorState = (message: string, actionLabel: string, action: () => void) => ( +
+

{message} 😞

+ +
+ ); + + if (fetchError || !data) { if (fetchError instanceof ApiError) { - try { - const errorData = JSON.parse(fetchError.message); - - if (errorData.status === 'NOT_FOUND') { - return

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

; - } - } catch (parseError) { - return

{`Error ${fetchError.message}`}

; + if (fetchError.status === 404) { + router.push('/404'); + return null; } + toast.error(fetchError.message || '🚫 에러가 발생했습니다.'); + } else if (fetchError) { + toast.error('🚫 데이터 통신에 실패했습니다.'); } - return

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

; - } - if (!data) { - return

데이터를 불러올 수 없습니다.

; + const errorMessage = fetchError + ? '데이터를 불러오는 데 실패했습니다.' + : '데이터를 불러올 수 없습니다.'; + + return renderErrorState(errorMessage, '다시 시도', refetch); } 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 72d638bc..59d9c11f 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,11 +3,11 @@ 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'; import ConfirmModal from '@/src/components/common/modal/confirm-modal'; +import GatheringSkeletonList from '@/src/components/common/skeleton/gathering-skeleton-list'; import CrewGatheringList from '@/src/components/gathering-list/crew-gathering-list'; interface GatheringListSectionProps { @@ -24,7 +24,7 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) await addLike(gatheringId); } catch (apiError) { if (apiError instanceof ApiError) { - toast.error(`찜하기에 실패했습니다: ${apiError.message}`); + toast.error(`찜하기에 실패했습니다`); } } }; @@ -34,7 +34,7 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) await removeLike(gatheringId); } catch (apiError) { if (apiError instanceof ApiError) { - toast.error(`찜하기 해제에 실패했습니다: ${apiError.message}`); + toast.error(`찜하기 해제에 실패했습니다`); } } }; @@ -48,11 +48,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) refetch(); }; - // TODO: 추후 에러, 로딩 수정 if (isLoading) return (
- +
); @@ -66,7 +65,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps) if (!gatheringList || gatheringList.length === 0) return (
-

데이터가 없습니다.

+
+

아직 등록된 약속이 없습니다!

+

새로운 약속을 만들어보세요! 🙌

+
); diff --git a/src/components/common/skeleton/crew-detail-skeleton/index.tsx b/src/components/common/skeleton/crew-detail-skeleton/index.tsx new file mode 100644 index 00000000..6dccf153 --- /dev/null +++ b/src/components/common/skeleton/crew-detail-skeleton/index.tsx @@ -0,0 +1,65 @@ +import { Skeleton } from '@mantine/core'; + +export default function CrewDetailSkeleton() { + return ( +
+ {/* 상단 이미지와 정보 영역 */} +
+ +
+ + + +
+
+ + +
+
+ + {/* 소개 및 참여 인원 영역 */} +
+ {/* 크루장, 소개 영역 */} +
+
+ +
+ + +
+
+
+ + + +
+
+ + {/* 참여 인원 영역 */} +
+
+
+ + + +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/components/common/skeleton/gathering-skeleton-list/index.tsx b/src/components/common/skeleton/gathering-skeleton-list/index.tsx index 23a18a66..42c2ae3d 100644 --- a/src/components/common/skeleton/gathering-skeleton-list/index.tsx +++ b/src/components/common/skeleton/gathering-skeleton-list/index.tsx @@ -7,7 +7,7 @@ interface GatheringSkeletonListProps { export default function GatheringSkeletonList({ num }: GatheringSkeletonListProps) { return (
{[...Array(num)].map((_, index) => ( diff --git a/src/components/common/skeleton/gathering-skeleton/index.tsx b/src/components/common/skeleton/gathering-skeleton/index.tsx index ec23a40d..d26d5f34 100644 --- a/src/components/common/skeleton/gathering-skeleton/index.tsx +++ b/src/components/common/skeleton/gathering-skeleton/index.tsx @@ -2,14 +2,20 @@ import { Skeleton } from '@mantine/core'; export default function GatheringSkeleton() { return ( -
- -
- - - - - +
+
+ +
+
+
+ + + +
+
+ +
+
);