Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -114,29 +111,35 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) {
});
};

// TODO: ๋กœ๋”ฉ, ์—๋Ÿฌ์ฒ˜๋ฆฌ ์ถ”ํ›„ ๊ฐœ์„ 
if (isLoading) {
return <Loader />;
return <CrewDetailSkeleton />;
}

// TODO: ์ถ”ํ›„ 404ํŽ˜์ด์ง€๋กœ ์ด๋™์‹œํ‚ค๊ธฐ
if (fetchError) {
const renderErrorState = (message: string, actionLabel: string, action: () => void) => (
<div className="flex h-screen flex-col items-center justify-center">
<p className="mb-4 text-gray-500">{message} ๐Ÿ˜ž</p>
<Button className="btn-filled" onClick={action}>
{actionLabel}
</Button>
</div>
);

if (fetchError || !data) {
if (fetchError instanceof ApiError) {
try {
const errorData = JSON.parse(fetchError.message);

if (errorData.status === 'NOT_FOUND') {
return <p>ํฌ๋ฃจ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</p>;
}
} catch (parseError) {
return <p>{`Error ${fetchError.message}`}</p>;
if (fetchError.status === 404) {
router.push('/404');
return null;
}
toast.error(fetchError.message || '๐Ÿšซ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
} else if (fetchError) {
toast.error('๐Ÿšซ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
return <p>๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.</p>;
}

if (!data) {
return <p>๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>;
const errorMessage = fetchError
? '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'
: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.';

return renderErrorState(errorMessage, '๋‹ค์‹œ ์‹œ๋„', refetch);
Comment on lines +118 to +142
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ› ๏ธ Refactor suggestion

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐœ์„ ๋˜์—ˆ์ง€๋งŒ, ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ๊ณ ๋ คํ•ด๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค:

  1. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์ˆ˜๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ค‘์•™ ๊ด€๋ฆฌ
  2. ApiError ํƒ€์ž… ์ฒดํฌ ๋กœ์ง ๊ฐœ์„ 

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐœ์„ ํ•ด๋ณด์„ธ์š”:

+const ERROR_MESSAGES = {
+  FETCH_FAILED: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
+  NO_DATA: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.',
+  NETWORK_ERROR: '๐Ÿšซ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
+} as const;

-    if (fetchError instanceof ApiError) {
-      if (fetchError.status === 404) {
+    if (fetchError instanceof ApiError && fetchError.status === 404) {
         router.push('/404');
         return null;
-      }
-      toast.error(fetchError.message || '๐Ÿšซ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
-    } else if (fetchError) {
-      toast.error('๐Ÿšซ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
     }
+    
+    const errorMessage = fetchError instanceof ApiError
+      ? fetchError.message
+      : ERROR_MESSAGES.NETWORK_ERROR;
+    
+    toast.error(errorMessage);
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const renderErrorState = (message: string, actionLabel: string, action: () => void) => (
<div className="flex h-screen flex-col items-center justify-center">
<p className="mb-4 text-gray-500">{message} ๐Ÿ˜ž</p>
<Button className="btn-filled" onClick={action}>
{actionLabel}
</Button>
</div>
);
if (fetchError || !data) {
if (fetchError instanceof ApiError) {
try {
const errorData = JSON.parse(fetchError.message);
if (errorData.status === 'NOT_FOUND') {
return <p>ํฌ๋ฃจ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</p>;
}
} catch (parseError) {
return <p>{`Error ${fetchError.message}`}</p>;
if (fetchError.status === 404) {
router.push('/404');
return null;
}
toast.error(fetchError.message || '๐Ÿšซ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
} else if (fetchError) {
toast.error('๐Ÿšซ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
return <p>๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.</p>;
}
if (!data) {
return <p>๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>;
const errorMessage = fetchError
? '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'
: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.';
return renderErrorState(errorMessage, '๋‹ค์‹œ ์‹œ๋„', refetch);
const ERROR_MESSAGES = {
FETCH_FAILED: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
NO_DATA: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.',
NETWORK_ERROR: '๐Ÿšซ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
} as const;
const renderErrorState = (message: string, actionLabel: string, action: () => void) => (
<div className="flex h-screen flex-col items-center justify-center">
<p className="mb-4 text-gray-500">{message} ๐Ÿ˜ž</p>
<Button className="btn-filled" onClick={action}>
{actionLabel}
</Button>
</div>
);
if (fetchError || !data) {
if (fetchError instanceof ApiError && fetchError.status === 404) {
router.push('/404');
return null;
}
const errorMessage = fetchError instanceof ApiError
? fetchError.message
: ERROR_MESSAGES.NETWORK_ERROR;
toast.error(errorMessage);
const errorMessage = fetchError
? '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'
: '๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.';
return renderErrorState(errorMessage, '๋‹ค์‹œ ์‹œ๋„', refetch);

}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(`์ฐœํ•˜๊ธฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค`);
}
}
};
Expand All @@ -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(`์ฐœํ•˜๊ธฐ ํ•ด์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค`);
}
}
};
Expand All @@ -48,11 +48,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
refetch();
};

// TODO: ์ถ”ํ›„ ์—๋Ÿฌ, ๋กœ๋”ฉ ์ˆ˜์ •
if (isLoading)
return (
<div className="flex items-center justify-center">
<Loader />
<GatheringSkeletonList num={3} />
</div>
);

Expand All @@ -66,7 +65,10 @@ export default function GatheringListSection({ id }: GatheringListSectionProps)
if (!gatheringList || gatheringList.length === 0)
return (
<div className="flex items-center justify-center">
<p>๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
<div className="flex h-[380px] flex-col items-center justify-center">
<p className="text-xl font-semibold">์•„์ง ๋“ฑ๋ก๋œ ์•ฝ์†์ด ์—†์Šต๋‹ˆ๋‹ค!</p>
<p className="mt-2 text-base font-medium text-blue-400">์ƒˆ๋กœ์šด ์•ฝ์†์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿ™Œ</p>
</div>
</div>
);

Expand Down
65 changes: 65 additions & 0 deletions src/components/common/skeleton/crew-detail-skeleton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Skeleton } from '@mantine/core';

export default function CrewDetailSkeleton() {
return (
<div className="mx-auto flex max-w-[1200px] flex-col gap-6">
{/* ์ƒ๋‹จ ์ด๋ฏธ์ง€์™€ ์ •๋ณด ์˜์—ญ */}
<div className="relative h-96 w-full overflow-hidden rounded-lg p-6 shadow-sm">
<Skeleton className="absolute inset-0 h-full w-full" />
<div className="absolute bottom-6 left-6 space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-6 w-64" />
<Skeleton className="h-10 w-32" />
</div>
<div className="absolute bottom-6 right-6 flex items-center space-x-2">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="h-6 w-6 rounded-full" />
</div>
</div>

{/* ์†Œ๊ฐœ ๋ฐ ์ฐธ์—ฌ ์ธ์› ์˜์—ญ */}
<div className="flex flex-col gap-4 md:flex-row lg:flex-1">
{/* ํฌ๋ฃจ์žฅ, ์†Œ๊ฐœ ์˜์—ญ */}
<div className="md:basis-4/7 flex h-64 w-full flex-col space-y-4 rounded-lg p-4 shadow-sm">
<div className="flex items-center space-x-3">
<Skeleton className="h-14 w-14 rounded-full" />
<div className="flex flex-col space-y-1">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-40" />
</div>
</div>
<div className="w-full border-t border-gray-200 pt-4">
<Skeleton className="mb-2 h-5 w-24" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</div>

{/* ์ฐธ์—ฌ ์ธ์› ์˜์—ญ */}
<div className="md:basis-3/7 flex h-64 w-full flex-col rounded-lg p-4 shadow-sm">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="mx-2 h-4 w-12" />
<Skeleton className="h-4 w-12" />
</div>
<Skeleton className="h-6 w-24" />
</div>
<Skeleton className="h-4 w-full" />
<div className="mt-4 h-40 space-y-4 overflow-y-auto">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Skeleton className="h-10 w-10 rounded-full" />
<Skeleton className="h-4 w-20" />
</div>
<div className="flex items-center space-x-2">
<Skeleton className="h-10 w-10 rounded-full" />
<Skeleton className="h-4 w-20" />
</div>
</div>
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface GatheringSkeletonListProps {
export default function GatheringSkeletonList({ num }: GatheringSkeletonListProps) {
return (
<div
className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
className="grid w-full gap-4 md:grid-cols-2 lg:max-w-[1200px] lg:grid-cols-3"
aria-label="์ฝ˜ํ…์ธ  ๋กœ๋”ฉ ์ค‘"
>
{[...Array(num)].map((_, index) => (
Expand Down
22 changes: 14 additions & 8 deletions src/components/common/skeleton/gathering-skeleton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { Skeleton } from '@mantine/core';

export default function GatheringSkeleton() {
return (
<div className="flex flex-col overflow-hidden rounded-xl">
<Skeleton className="h-[160px] w-full" />
<div className="relative flex min-h-[184px] flex-col gap-2 p-4">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-24" />
<Skeleton circle className="absolute right-4 top-4 h-8 w-8" />
<Skeleton className="absolute bottom-4 left-4 right-4 h-10 w-auto" />
<div className="relative h-[380px] w-full overflow-hidden rounded-lg bg-white shadow-sm">
<div className="relative h-40 w-full">
<Skeleton className="h-full w-full rounded-t-lg" />
</div>
<div className="flex min-h-[220px] flex-col justify-between p-4">
<div>
<Skeleton className="mb-2 h-6 w-1/3" />
<Skeleton className="mb-4 h-6 w-2/3" />
<Skeleton className="h-4 w-1/2" />
</div>
<div className="mt-6">
<Skeleton className="h-4 w-1/3" />
<div className="mt-4 h-10" />
</div>
</div>
</div>
);
Expand Down
Loading