Skip to content

Commit 6971d02

Browse files
authored
Merge pull request #155 from CodeitFESI4-Team1/Feat/153/DetailPageLoading
Feat/153/detail page loading
2 parents 3a512b6 + 0baa8cb commit 6971d02

File tree

6 files changed

+233
-37
lines changed

6 files changed

+233
-37
lines changed

package-lock.json

Lines changed: 120 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import { useEffect, useState } from 'react';
44
import { toast } from 'react-toastify';
55
import { useRouter } from 'next/navigation';
6-
import { Loader } from '@mantine/core';
76
import { useDisclosure } from '@mantine/hooks';
87
import { cancelCrew, joinCrew, leaveCrew } from '@/src/_apis/crew/crew-detail-apis';
98
import { useUser } from '@/src/_queries/auth/user-queries';
109
import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries';
1110
import { ApiError } from '@/src/utils/api';
11+
import Button from '@/src/components/common/input/button';
1212
import ConfirmCancelModal from '@/src/components/common/modal/confirm-cancel-modal';
13+
import CrewDetailSkeleton from '@/src/components/common/skeleton/crew-detail-skeleton';
1314
import { User } from '@/src/types/auth';
1415
import DetailCrewPresenter from './detail-crew-presenter';
1516

@@ -38,12 +39,8 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) {
3839

3940
useEffect(() => {
4041
if (data) {
41-
// confirmed 상태 계산
42-
if (data.participantCount !== undefined && data.totalCount !== undefined) {
43-
setIsConfirmed(data.participantCount === data.totalCount);
44-
}
42+
setIsConfirmed(data.participantCount === data.totalCount);
4543

46-
// Captain 및 멤버 여부 확인 (currentUserId 필요)
4744
if (currentUserId) {
4845
const captain = data.crewMembers.find((member) => member.captain);
4946
const memberExists = data.crewMembers.some((member) => member.id === currentUserId);
@@ -114,29 +111,35 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) {
114111
});
115112
};
116113

117-
// TODO: 로딩, 에러처리 추후 개선
118114
if (isLoading) {
119-
return <Loader />;
115+
return <CrewDetailSkeleton />;
120116
}
121117

122-
// TODO: 추후 404페이지로 이동시키기
123-
if (fetchError) {
118+
const renderErrorState = (message: string, actionLabel: string, action: () => void) => (
119+
<div className="flex h-screen flex-col items-center justify-center">
120+
<p className="mb-4 text-gray-500">{message} 😞</p>
121+
<Button className="btn-filled" onClick={action}>
122+
{actionLabel}
123+
</Button>
124+
</div>
125+
);
126+
127+
if (fetchError || !data) {
124128
if (fetchError instanceof ApiError) {
125-
try {
126-
const errorData = JSON.parse(fetchError.message);
127-
128-
if (errorData.status === 'NOT_FOUND') {
129-
return <p>크루 정보를 찾을 수 없습니다</p>;
130-
}
131-
} catch (parseError) {
132-
return <p>{`Error ${fetchError.message}`}</p>;
129+
if (fetchError.status === 404) {
130+
router.push('/404');
131+
return null;
133132
}
133+
toast.error(fetchError.message || '🚫 에러가 발생했습니다.');
134+
} else if (fetchError) {
135+
toast.error('🚫 데이터 통신에 실패했습니다.');
134136
}
135-
return <p>데이터 통신에 실패했습니다.</p>;
136-
}
137137

138-
if (!data) {
139-
return <p>데이터를 불러올 수 없습니다.</p>;
138+
const errorMessage = fetchError
139+
? '데이터를 불러오는 데 실패했습니다.'
140+
: '데이터를 불러올 수 없습니다.';
141+
142+
return renderErrorState(errorMessage, '다시 시도', refetch);
140143
}
141144

142145
return (

src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import { useState } from 'react';
44
import { toast } from 'react-toastify';
55
import { useRouter } from 'next/navigation';
6-
import { Loader } from '@mantine/core';
76
import { addLike, removeLike } from '@/src/_apis/liked/liked-apis';
87
import { useGetGatheringListQuery } from '@/src/_queries/crew/gathering-list-queries';
98
import { ApiError } from '@/src/utils/api';
109
import ConfirmModal from '@/src/components/common/modal/confirm-modal';
10+
import GatheringSkeletonList from '@/src/components/common/skeleton/gathering-skeleton-list';
1111
import CrewGatheringList from '@/src/components/gathering-list/crew-gathering-list';
1212

1313
interface GatheringListSectionProps {
@@ -24,7 +24,7 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
2424
await addLike(gatheringId);
2525
} catch (apiError) {
2626
if (apiError instanceof ApiError) {
27-
toast.error(`찜하기에 실패했습니다: ${apiError.message}`);
27+
toast.error(`찜하기에 실패했습니다`);
2828
}
2929
}
3030
};
@@ -34,7 +34,7 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
3434
await removeLike(gatheringId);
3535
} catch (apiError) {
3636
if (apiError instanceof ApiError) {
37-
toast.error(`찜하기 해제에 실패했습니다: ${apiError.message}`);
37+
toast.error(`찜하기 해제에 실패했습니다`);
3838
}
3939
}
4040
};
@@ -48,11 +48,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
4848
refetch();
4949
};
5050

51-
// TODO: 추후 에러, 로딩 수정
5251
if (isLoading)
5352
return (
5453
<div className="flex items-center justify-center">
55-
<Loader />
54+
<GatheringSkeletonList num={3} />
5655
</div>
5756
);
5857

@@ -66,7 +65,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
6665
if (!gatheringList || gatheringList.length === 0)
6766
return (
6867
<div className="flex items-center justify-center">
69-
<p>데이터가 없습니다.</p>
68+
<div className="flex h-[380px] flex-col items-center justify-center">
69+
<p className="text-xl font-semibold">아직 등록된 약속이 없습니다!</p>
70+
<p className="mt-2 text-base font-medium text-blue-400">새로운 약속을 만들어보세요! 🙌</p>
71+
</div>
7072
</div>
7173
);
7274

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Skeleton } from '@mantine/core';
2+
3+
export default function CrewDetailSkeleton() {
4+
return (
5+
<div className="mx-auto flex max-w-[1200px] flex-col gap-6">
6+
{/* 상단 이미지와 정보 영역 */}
7+
<div className="relative h-96 w-full overflow-hidden rounded-lg p-6 shadow-sm">
8+
<Skeleton className="absolute inset-0 h-full w-full" />
9+
<div className="absolute bottom-6 left-6 space-y-4">
10+
<Skeleton className="h-8 w-48" />
11+
<Skeleton className="h-6 w-64" />
12+
<Skeleton className="h-10 w-32" />
13+
</div>
14+
<div className="absolute bottom-6 right-6 flex items-center space-x-2">
15+
<Skeleton className="h-6 w-6 rounded-full" />
16+
<Skeleton className="h-6 w-6 rounded-full" />
17+
</div>
18+
</div>
19+
20+
{/* 소개 및 참여 인원 영역 */}
21+
<div className="flex flex-col gap-4 md:flex-row lg:flex-1">
22+
{/* 크루장, 소개 영역 */}
23+
<div className="md:basis-4/7 flex h-64 w-full flex-col space-y-4 rounded-lg p-4 shadow-sm">
24+
<div className="flex items-center space-x-3">
25+
<Skeleton className="h-14 w-14 rounded-full" />
26+
<div className="flex flex-col space-y-1">
27+
<Skeleton className="h-6 w-32" />
28+
<Skeleton className="h-4 w-40" />
29+
</div>
30+
</div>
31+
<div className="w-full border-t border-gray-200 pt-4">
32+
<Skeleton className="mb-2 h-5 w-24" />
33+
<Skeleton className="h-4 w-full" />
34+
<Skeleton className="h-4 w-full" />
35+
</div>
36+
</div>
37+
38+
{/* 참여 인원 영역 */}
39+
<div className="md:basis-3/7 flex h-64 w-full flex-col rounded-lg p-4 shadow-sm">
40+
<div className="mb-2 flex items-center justify-between">
41+
<div className="flex items-center">
42+
<Skeleton className="h-6 w-6 rounded-full" />
43+
<Skeleton className="mx-2 h-4 w-12" />
44+
<Skeleton className="h-4 w-12" />
45+
</div>
46+
<Skeleton className="h-6 w-24" />
47+
</div>
48+
<Skeleton className="h-4 w-full" />
49+
<div className="mt-4 h-40 space-y-4 overflow-y-auto">
50+
<div className="grid grid-cols-2 gap-4">
51+
<div className="flex items-center space-x-2">
52+
<Skeleton className="h-10 w-10 rounded-full" />
53+
<Skeleton className="h-4 w-20" />
54+
</div>
55+
<div className="flex items-center space-x-2">
56+
<Skeleton className="h-10 w-10 rounded-full" />
57+
<Skeleton className="h-4 w-20" />
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
}

src/components/common/skeleton/gathering-skeleton-list/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface GatheringSkeletonListProps {
77
export default function GatheringSkeletonList({ num }: GatheringSkeletonListProps) {
88
return (
99
<div
10-
className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
10+
className="grid w-full gap-4 md:grid-cols-2 lg:max-w-[1200px] lg:grid-cols-3"
1111
aria-label="콘텐츠 로딩 중"
1212
>
1313
{[...Array(num)].map((_, index) => (

src/components/common/skeleton/gathering-skeleton/index.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import { Skeleton } from '@mantine/core';
22

33
export default function GatheringSkeleton() {
44
return (
5-
<div className="flex flex-col overflow-hidden rounded-xl">
6-
<Skeleton className="h-[160px] w-full" />
7-
<div className="relative flex min-h-[184px] flex-col gap-2 p-4">
8-
<Skeleton className="h-4 w-40" />
9-
<Skeleton className="h-4 w-32" />
10-
<Skeleton className="h-4 w-24" />
11-
<Skeleton circle className="absolute right-4 top-4 h-8 w-8" />
12-
<Skeleton className="absolute bottom-4 left-4 right-4 h-10 w-auto" />
5+
<div className="relative h-[380px] w-full overflow-hidden rounded-lg bg-white shadow-sm">
6+
<div className="relative h-40 w-full">
7+
<Skeleton className="h-full w-full rounded-t-lg" />
8+
</div>
9+
<div className="flex min-h-[220px] flex-col justify-between p-4">
10+
<div>
11+
<Skeleton className="mb-2 h-6 w-1/3" />
12+
<Skeleton className="mb-4 h-6 w-2/3" />
13+
<Skeleton className="h-4 w-1/2" />
14+
</div>
15+
<div className="mt-6">
16+
<Skeleton className="h-4 w-1/3" />
17+
<div className="mt-4 h-10" />
18+
</div>
1319
</div>
1420
</div>
1521
);

0 commit comments

Comments
 (0)