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
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand All @@ -35,7 +36,7 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

*storybook.log

# storybook
/storybook-static
/storybook-static
*storybook.log
8 changes: 0 additions & 8 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,5 @@ const nextConfig = {
},
];
},
async rewrites() {
return [
{
source: '/:path*',
destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`,
},
];
},
};
export default nextConfig;
9 changes: 5 additions & 4 deletions src/_apis/detail/get-crew-detail.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { fetchApi } from '@/src/utils/api';
import { CrewDetail } from '@/src/types/crew-card';

export async function getCrewDetail(): Promise<{ data: CrewDetail }> {
const response = await fetchApi<CrewDetail[]>('/crewDetail', {
export async function getCrewDetail(id: number): Promise<CrewDetail> {
const url = `/api/crews/${id}`;

const response = await fetchApi<{ data: CrewDetail }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

return { data: response[0] };
return response.data;
Comment on lines +7 to +13
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 로직 추가 검토 필요

API 호출 실패 시의 에러 처리가 명시적으로 구현되어 있지 않습니다. 사용자 경험 향상을 위해 에러 처리 로직 추가를 고려해 주세요.

다음과 같은 구현을 제안드립니다:

 export async function getCrewDetail(id: number): Promise<CrewDetail> {
   const url = `/api/crews/${id}`;

-  const response = await fetchApi<{ data: CrewDetail }>(url, {
-    method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-  });
-  return response.data;
+  try {
+    const response = await fetchApi<{ data: CrewDetail }>(url, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+    });
+    return response.data;
+  } catch (error) {
+    console.error('크루 상세 정보를 가져오는데 실패했습니다:', error);
+    throw new Error('크루 상세 정보를 불러올 수 없습니다.');
+  }
 }
📝 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 response = await fetchApi<{ data: CrewDetail }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return { data: response[0] };
return response.data;
try {
const response = await fetchApi<{ data: CrewDetail }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('크루 상세 정보를 가져오는데 실패했습니다:', error);
throw new Error('크루 상세 정보를 불러올 수 없습니다.');
}

}
17 changes: 17 additions & 0 deletions src/_apis/detail/get-gathering-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { fetchApi } from '@/src/utils/api';
import { GatheringDetailType } from '@/src/types/gathering-data';

export async function GetGatheringDetail(
crewId: number,
gatheringId: number,
): Promise<GatheringDetailType> {
Comment on lines +4 to +7
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

함수 이름이 컨벤션을 따르지 않습니다

함수 이름이 파스칼 케이스(PascalCase)로 작성되어 있습니다. JavaScript/TypeScript에서 함수는 일반적으로 카멜 케이스(camelCase)를 사용합니다.

다음과 같이 수정하는 것을 제안합니다:

-export async function GetGatheringDetail(
+export async function getGatheringDetail(
   crewId: number,
   gatheringId: number,
 ): Promise<GatheringDetailType>
📝 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
export async function GetGatheringDetail(
crewId: number,
gatheringId: number,
): Promise<GatheringDetailType> {
export async function getGatheringDetail(
crewId: number,
gatheringId: number,
): Promise<GatheringDetailType> {

const url = `/api/crews/${crewId}/gatherings/${gatheringId}`;

const response = await fetchApi<{ data: GatheringDetailType }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
Comment on lines +10 to +15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리 로직이 필요합니다

API 호출 시 발생할 수 있는 다양한 에러 상황(네트워크 오류, 서버 오류 등)에 대한 처리가 없습니다.

다음과 같은 에러 처리를 추가하는 것을 제안합니다:

-  const response = await fetchApi<{ data: GatheringDetailType }>(url, {
-    method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-  });
+  try {
+    const response = await fetchApi<{ data: GatheringDetailType }>(url, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+    });
+    return response.data;
+  } catch (error) {
+    if (error instanceof Error) {
+      throw new Error(`모임 상세 정보를 가져오는데 실패했습니다: ${error.message}`);
+    }
+    throw new Error('모임 상세 정보를 가져오는데 실패했습니다');
+  }
📝 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 response = await fetchApi<{ data: GatheringDetailType }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
try {
const response = await fetchApi<{ data: GatheringDetailType }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
if (error instanceof Error) {
throw new Error(`모임 상세 정보를 가져오는데 실패했습니다: ${error.message}`);
}
throw new Error('모임 상세 정보를 가져오는데 실패했습니다');
}

return response.data;
}
7 changes: 5 additions & 2 deletions src/_apis/detail/get-gathering-list.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { fetchApi } from '@/src/utils/api';
import { GatheringType } from '@/src/types/gathering-data';

export async function getGatheringList(): Promise<GatheringType[]> {
return fetchApi<GatheringType[]>('/gatherings', {
export async function getGatheringList(id: number): Promise<GatheringType[]> {
const url = `/api/crews/${id}/gatherings`;

const response = await fetchApi<{ data: GatheringType[] }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
Comment on lines +5 to +13
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 응답 처리 개선이 필요합니다.

API 응답 처리에서 몇 가지 개선이 필요합니다:

  1. 오류 처리가 누락되어 있습니다.
  2. API 응답의 유효성 검사가 없습니다.

다음과 같이 개선하는 것을 제안드립니다:

 export async function getGatheringList(id: number): Promise<GatheringType[]> {
   const url = `/api/crews/${id}/gatherings`;

+  try {
     const response = await fetchApi<{ data: GatheringType[] }>(url, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json',
       },
     });
+    
+    if (!response.data) {
+      throw new Error('데이터가 존재하지 않습니다');
+    }
+
     return response.data;
+  } catch (error) {
+    console.error('모임 목록을 가져오는데 실패했습니다:', error);
+    throw error;
+  }
 }

또한 API 응답의 타입 안전성을 위해 zod와 같은 런타임 타입 검증 라이브러리의 사용을 고려해보시는 것을 추천드립니다.

📝 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 url = `/api/crews/${id}/gatherings`;
const response = await fetchApi<{ data: GatheringType[] }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
export async function getGatheringList(id: number): Promise<GatheringType[]> {
const url = `/api/crews/${id}/gatherings`;
try {
const response = await fetchApi<{ data: GatheringType[] }>(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.data) {
throw new Error('데이터가 존재하지 않습니다');
}
return response.data;
} catch (error) {
console.error('모임 목록을 가져오는데 실패했습니다:', error);
throw error;
}
}

}
9 changes: 9 additions & 0 deletions src/_queries/detail/crew-detail-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { getCrewDetail } from '@/src/_apis/detail/get-crew-detail';

export function useGetCrewDetailQuery(id: number) {
return useQuery({
queryKey: ['crewDetail', id],
queryFn: () => getCrewDetail(id),
});
}
9 changes: 9 additions & 0 deletions src/_queries/detail/gathering-detail-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { GetGatheringDetail } from '@/src/_apis/detail/get-gathering-detail';

export function useGetGatheringDetailQuery(crewId: number, gatheringId: number) {
return useQuery({
queryKey: ['gatheringDetail', crewId, gatheringId],
queryFn: () => GetGatheringDetail(crewId, gatheringId),
});
}
11 changes: 11 additions & 0 deletions src/_queries/detail/gathering-list-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { getGatheringList } from '@/src/_apis/detail/get-gathering-list';
import { GatheringType } from '@/src/types/gathering-data';

export function useGetGatheringListQuery(id: number) {
return useQuery<GatheringType[], Error>({
queryKey: ['gatheringList', id],
queryFn: () => getGatheringList(id),
enabled: !!id,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useGetCrewDetailQuery } from '@/src/_queries/detail/crew-detail-queries';
import { ApiError } from '@/src/utils/api';
import DetailCrewCard from '@/src/components/common/crew-list/detail-crew-card';

interface DetailCrewSectionProps {
id: number;
}

export default function DetailCrewSection({ id }: DetailCrewSectionProps) {
const { data, isLoading, error } = useGetCrewDetailQuery(id);

// TODO: 추후 에러 처리 수정
if (isLoading) return <p>Loading...</p>;

if (error) {
if (error instanceof ApiError) {
try {
const errorData = JSON.parse(error.message);

if (errorData.status === 'NOT_FOUND') {
return <p>크루 정보를 찾을 수 없습니다</p>;
}
} catch {
return <p>{`Error ${error.status}: ${error.message}`}</p>;
}
}
return <p>데이터 통신에 실패했습니다.</p>;
}
Comment on lines +17 to +30
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 로직 개선 필요

현재 에러 처리 로직이 복잡하고 중첩된 조건문을 사용하고 있어 유지보수가 어려울 수 있습니다.

다음과 같이 에러 처리를 단순화하는 것을 제안합니다:

-  if (error) {
-    if (error instanceof ApiError) {
-      try {
-        const errorData = JSON.parse(error.message);
-
-        if (errorData.status === 'NOT_FOUND') {
-          return <p>크루 정보를 찾을 수 없습니다</p>;
-        }
-      } catch {
-        return <p>{`Error ${error.status}: ${error.message}`}</p>;
-      }
-    }
-    return <p>데이터 통신에 실패했습니다.</p>;
-  }
+  if (error) {
+    const getErrorMessage = () => {
+      if (!(error instanceof ApiError)) {
+        return '데이터 통신에 실패했습니다.';
+      }
+      try {
+        const errorData = JSON.parse(error.message);
+        return errorData.status === 'NOT_FOUND'
+          ? '크루 정보를 찾을 수 없습니다'
+          : `Error ${error.status}: ${error.message}`;
+      } catch {
+        return `Error ${error.status}: ${error.message}`;
+      }
+    };
+    return <p>{getErrorMessage()}</p>;
+  }
📝 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
if (error) {
if (error instanceof ApiError) {
try {
const errorData = JSON.parse(error.message);
if (errorData.status === 'NOT_FOUND') {
return <p>크루 정보를 찾을 수 없습니다</p>;
}
} catch {
return <p>{`Error ${error.status}: ${error.message}`}</p>;
}
}
return <p>데이터 통신에 실패했습니다.</p>;
}
if (error) {
const getErrorMessage = () => {
if (!(error instanceof ApiError)) {
return '데이터 통신에 실패했습니다.';
}
try {
const errorData = JSON.parse(error.message);
return errorData.status === 'NOT_FOUND'
? '크루 정보를 찾을 수 없습니다'
: `Error ${error.status}: ${error.message}`;
} catch {
return `Error ${error.status}: ${error.message}`;
}
};
return <p>{getErrorMessage()}</p>;
}


// data가 있을 때만 DetailCrewCard를 렌더링
return data ? <DetailCrewCard data={data} /> : null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { useGetGatheringListQuery } from '@/src/_queries/detail/gathering-list-queries';
import GatheringCardCarousel from '@/src/components/gathering-list/gathering-card-carousel';

interface GatheringListSectionProps {
id: number;
}

export default function GatheringListSection({ id }: GatheringListSectionProps) {
const { data: gatheringList, isLoading, error } = useGetGatheringListQuery(id);

if (isLoading) return <p>로딩 중...</p>;

if (error) return <p>데이터를 불러오는 데 실패했습니다: {error.message}</p>;

if (!gatheringList || gatheringList.length === 0) return <p>데이터가 없습니다.</p>;

return <GatheringCardCarousel gatheringData={gatheringList} crewId={id} />;
}
26 changes: 14 additions & 12 deletions src/app/(crew)/crew/detail/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
import { getCrewDetail } from '@/src/_apis/detail/get-crew-detail';
import { getGatheringList } from '@/src/_apis/detail/get-gathering-list';
import DetailCrewCard from '@/src/components/common/crew-list/detail-crew-card';
import GatheringCardCarousel from '@/src/components/gathering-list/gathering-card-carousel';
import CreateGathering from './_components/create-gathering';
import DetailCrewSection from './_components/detail-crew-section';
import GatheringListSection from './_components/gathering-list-section';
import CrewReviewSection from './_components/review-section';

export default async function CrewDetailPage() {
const { data: crewDetail } = await getCrewDetail();
const gatheringList = await getGatheringList();
interface CrewDetailPageProps {
params: { id: string };
}

export default async function CrewDetailPage({ params }: CrewDetailPageProps) {
const id = Number(params.id);
Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

params.id의 타입 변환 처리를 개선해 주세요.

Number() 함수를 직접 사용하면 잘못된 id 형식에 대한 처리가 누락될 수 있습니다.

다음과 같이 개선하는 것을 추천드립니다:

-  const id = Number(params.id);
+  const id = parseInt(params.id, 10);
+  if (isNaN(id)) {
+    throw new Error('Invalid ID format');
+  }
📝 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
export default async function CrewDetailPage({ params }: CrewDetailPageProps) {
const id = Number(params.id);
export default async function CrewDetailPage({ params }: CrewDetailPageProps) {
const id = parseInt(params.id, 10);
if (isNaN(id)) {
throw new Error('Invalid ID format');
}


return (
<div className="mx-auto min-h-screen w-full max-w-full overflow-x-hidden">
<div className="mx-3 my-7 space-y-10 md:mx-7 md:my-11 lg:mx-11 lg:my-16">
<section className="w-full">
<article>
{/* //TODO: 추후 confirmed부분 수정 */}
<DetailCrewCard isConfirmed={false} {...crewDetail} />
<DetailCrewSection id={id} />
</article>
</section>
<section className="w-full space-y-6">
<article className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold">크루 약속</h2>
<CreateGathering />
{/* <CreateGathering /> */}
</div>
<div className="flex w-full">
<GatheringCardCarousel gatheringData={gatheringList} />
<GatheringListSection id={id} />
</div>
</article>
</section>
<section className="w-full">
{/* // TODO: 리뷰 완성되면 수정 */}
{/* <section className="w-full">
<article className="space-y-6">
<h2 className="text-2xl font-semibold">크루 리뷰</h2>
<CrewReviewSection />
</article>
</section>
</section> */}
</div>
</div>
);
Expand Down
90 changes: 53 additions & 37 deletions src/components/common/crew-list/detail-crew-card.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// CrewCard 스토리북 파일
import type { Meta, StoryObj } from '@storybook/react';
import CrewCard from './detail-crew-card';
import DetailCrewCard from './detail-crew-card';

const meta: Meta = {
title: 'Components/CrewCardList/CrewCard',
component: CrewCard,
title: 'Components/CrewCardList/DetailCrewCard',
component: DetailCrewCard,
parameters: {
layout: 'centered',
nextjs: {
Expand All @@ -13,50 +12,67 @@ const meta: Meta = {
},
tags: ['autodocs'],
args: {
id: 0,
title: '같이 물장구칠사람',
mainLocation: '대전광역시',
subLocation: '유성구',
imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg',
totalGatheringCount: 5, // 기본 값 추가
crewMembers: [
{
id: 1,
nickname: 'John',
profileImageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg',
},
{
id: 2,
nickname: 'Jane',
profileImageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg',
},
], // 기본 프로필 리스트 추가
data: {
id: 1,
title: '같이 물장구칠사람',
mainLocation: '대전광역시',
subLocation: '유성구',
participantCount: 10,
totalCount: 20,
confirmed: true,
imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg',
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 URL을 상수로 분리하세요

하드코딩된 이미지 URL들이 여러 곳에서 반복되고 있습니다. 이는 유지보수를 어렵게 만들 수 있습니다.

스토리북 설정 파일에 상수로 분리하는 것을 제안합니다:

const MOCK_IMAGES = {
  CREW_DEFAULT: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg',
  PROFILE_DEFAULT: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg'
} as const;

Also applies to: 29-30, 35-36, 56-56, 73-73

totalGatheringCount: 5,
crewMembers: [
{
id: 1,
nickname: 'John',
profileImageUrl:
'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg',
},
{
id: 2,
nickname: 'Jane',
profileImageUrl:
'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg',
},
],
},
},
} satisfies Meta<typeof CrewCard>;
} satisfies Meta<typeof DetailCrewCard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
totalCount: 20,
participantCount: 10,
isConfirmed: true,
data: {
id: 1,
title: '같이 물장구칠사람',
mainLocation: '대전광역시',
subLocation: '유성구',
participantCount: 10,
totalCount: 20,
confirmed: true,
imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg',
totalGatheringCount: 5,
crewMembers: [], // 빈 배열이라도 기본값으로 설정
},
},
};

export const NotConfirmed: Story = {
args: {
totalCount: 10,
participantCount: 1,
isConfirmed: false,
},
};

export const Fulled: Story = {
args: {
totalCount: 5,
participantCount: 5,
isConfirmed: true,
data: {
id: 2,
title: '물장구 동호회',
mainLocation: '서울특별시',
subLocation: '강남구',
participantCount: 5,
totalCount: 15,
confirmed: false,
imageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg',
totalGatheringCount: 3,
crewMembers: [],
},
},
};
Loading