Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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