Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
18 changes: 18 additions & 0 deletions src/_apis/crew/my-crew-hosted-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fetchApi } from '@/src/utils/api';
import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card';

export async function getMyCrewHostedList(pageable: PageableTypes) {
const { page, size, sort = ['string'] } = pageable;

const response: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
},
);
return response.data;
}
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 호출 실패 시 적절한 에러 처리가 없습니다. 네트워크 오류나 서버 오류에 대한 처리를 추가하는 것이 좋겠습니다.

다음과 같이 에러 처리를 추가하는 것을 제안드립니다:

 export async function getMyCrewHostedList(pageable: PageableTypes) {
   const { page, size, sort = ['string'] } = pageable;

-  const response: { data: MyCrewListResponse } = await fetchApi(
-    `/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`,
-    {
-      method: 'GET',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      credentials: 'include', // 인증 정보를 요청에 포함
-    },
-  );
-  return response.data;
+  try {
+    const response: { data: MyCrewListResponse } = await fetchApi(
+      `/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`,
+      {
+        method: 'GET',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include',
+      },
+    );
+    return response.data;
+  } catch (error) {
+    if (error instanceof Error) {
+      throw new Error(`내가 주최한 크루 목록을 가져오는데 실패했습니다: ${error.message}`);
+    }
+    throw 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: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
},
);
return response.data;
}
export async function getMyCrewHostedList(pageable: PageableTypes) {
const { page, size, sort = ['string'] } = pageable;
try {
const response: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/hosted?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
},
);
return response.data;
} catch (error) {
if (error instanceof Error) {
throw new Error(`내가 주최한 크루 목록을 가져오는데 실패했습니다: ${error.message}`);
}
throw error;
}
}

18 changes: 18 additions & 0 deletions src/_apis/crew/my-crew-joined-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fetchApi } from '@/src/utils/api';
import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card';

export async function getMyCrewJoinedList(pageable: PageableTypes) {
const { page, size, sort = ['string'] } = pageable;

const response: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/joined?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
},
);
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

API 응답에 대한 런타임 타입 검증이 필요합니다

현재 MyCrewListResponse 타입은 TypeScript 인터페이스로만 정의되어 있어, 런타임에서 실제 API 응답이 예상된 형식과 일치하는지 확인할 수 없습니다. 다음과 같은 개선이 필요합니다:

  • zod 또는 유사한 런타임 타입 검증 라이브러리를 사용하여 API 응답 스키마를 정의하고 검증
  • src/utils/api.tsfetchApi 함수에서 응답 데이터 검증 로직 추가
  • MyCrewListResponse 타입에 대한 검증 스키마 구현

예시 코드:

import { z } from 'zod';

const MyCrewListSchema = z.object({
  content: z.array(MyCrewSchema),
  hasNext: z.boolean()
});

// fetchApi 호출 후 데이터 검증
const validatedData = MyCrewListSchema.parse(response.data);
🔗 Analysis chain

API 응답 타입 검증이 필요합니다.

API 응답의 타입 안전성을 보장하기 위해 런타임 타입 검증을 추가하는 것이 좋습니다.


Let's check the actual type definition and its usage to verify runtime type checking.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if MyCrewListResponse type is properly defined and used consistently

# Search for MyCrewListResponse type definition and usage
echo "Searching for type definition..."
rg "type MyCrewListResponse" -A 5

echo "Searching for type usage..."
rg "MyCrewListResponse"

Length of output: 1367


Script:

#!/bin/bash
# Check the MyCrewListResponse type definition and any runtime type validation
echo "Checking MyCrewListResponse type definition:"
rg -A 10 "interface MyCrewListResponse" src/types/crew-card.d.ts

echo "\nChecking for any runtime type validation (e.g., zod, io-ts):"
rg -l "zod|io-ts|runtypes|ajv|class-validator" 

echo "\nChecking fetchApi implementation for any type validation:"
rg -A 10 "function fetchApi" 

Length of output: 1523

return response.data;
}
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. API 호출 실패 시 에러 처리 로직 추가
  2. 영어로 된 코드베이스 일관성을 위해 한글 주석을 영어로 변경
  3. 페이지네이션 파라미터 유효성 검사 추가

다음과 같이 수정해보시는 건 어떨까요?:

 export async function getMyCrewJoinedList(pageable: PageableTypes) {
   const { page, size, sort = ['string'] } = pageable;
+
+  if (page < 0 || size <= 0) {
+    throw new Error('Invalid pagination parameters');
+  }
 
-  const response: { data: MyCrewListResponse } = await fetchApi(
-    `/api/crews/joined?page=${page}&size=${size}&sort=${sort}`,
-    {
-      method: 'GET',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      credentials: 'include', // 인증 정보를 요청에 포함
-    },
-  );
-  return response.data;
+  try {
+    const response: { data: MyCrewListResponse } = await fetchApi(
+      `/api/crews/joined?page=${page}&size=${size}&sort=${sort}`,
+      {
+        method: 'GET',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include', // Include authentication information
+      },
+    );
+    return response.data;
+  } catch (error) {
+    throw new Error(`Failed to fetch joined crew list: ${error.message}`);
+  }
 }
📝 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 getMyCrewJoinedList(pageable: PageableTypes) {
const { page, size, sort = ['string'] } = pageable;
const response: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/joined?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
},
);
return response.data;
}
export async function getMyCrewJoinedList(pageable: PageableTypes) {
const { page, size, sort = ['string'] } = pageable;
if (page < 0 || size <= 0) {
throw new Error('Invalid pagination parameters');
}
try {
const response: { data: MyCrewListResponse } = await fetchApi(
`/api/crews/joined?page=${page}&size=${size}&sort=${sort}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include authentication information
},
);
return response.data;
} catch (error) {
throw new Error(`Failed to fetch joined crew list: ${error.message}`);
}
}

29 changes: 19 additions & 10 deletions src/_queries/crew/crew-list-queries.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { getCrewList } from '@/src/_apis/crew/crew-list';
import { ConditionTypes, MainCrewListResponse, PageableTypes } from '@/src/types/crew-card';
import { getCrewList } from '../../_apis/crew/crew-list';

export function useGetCrewListQuery(condition: ConditionTypes) {
export function useGetCrewListQuery({
condition,
pageable,
}: {
condition: ConditionTypes;
pageable: PageableTypes;
}) {
const { size, sort = ['string'] } = pageable;
return {
queryKey: [
condition.keyword,
Expand All @@ -11,14 +18,16 @@ export function useGetCrewListQuery(condition: ConditionTypes) {
condition.sortType,
],
queryFn: ({ pageParam = 0 }) =>
getCrewList(condition, { page: pageParam, size: 6, sort: [condition.sortType] }).then(
(response) => {
if (response === undefined) {
throw new Error('Response is null');
}
return response;
},
),
getCrewList(condition, {
page: pageParam,
size,
sort,
}).then((response) => {
if (response === undefined) {
throw new Error('Response is undefined');
}
return response;
}),
getNextPageParam: (lastPage: MainCrewListResponse, allPages: MainCrewListResponse[]) =>
lastPage.hasNext ? allPages.length : undefined,
};
Expand Down
18 changes: 18 additions & 0 deletions src/_queries/crew/my-crew-hosted-list-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getMyCrewHostedList } from '@/src/_apis/crew/my-crew-hosted-list';
import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card';

export function useGetMyCrewHostedQuery({ pageable }: { pageable: PageableTypes }) {
const { size, sort = ['string'] } = pageable;
return {
queryKey: ['myCrewHosted'],
queryFn: ({ pageParam = 0 }) =>
getMyCrewHostedList({ page: pageParam, size, sort }).then((response) => {
if (response === undefined) {
throw new Error('크루 목록을 불러오는데 실패했습니다.');
}
return response;
}),
getNextPageParam: (lastPage: MyCrewListResponse, allPages: MyCrewListResponse[]) =>
lastPage.hasNext ? allPages.length : undefined,
};
}
18 changes: 18 additions & 0 deletions src/_queries/crew/my-crew-joined-list-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getMyCrewJoinedList } from '@/src/_apis/crew/my-crew-joined-list';
import { MyCrewListResponse, PageableTypes } from '@/src/types/crew-card';

export function useGetMyCrewJoinedQuery({ pageable }: { pageable: PageableTypes }) {
const { size, sort = ['string'] } = pageable;
return {
Comment on lines +4 to +6
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

정렬 파라미터를 더 유연하게 설정하세요.

현재 정렬 파라미터가 ['string']으로 고정되어 있어 유연성이 떨어집니다. 이를 외부에서 입력받을 수 있도록 변경하는 것이 좋겠습니다. 예를 들어, 기본값을 ['createdAt,desc']로 설정하고, 필요에 따라 변경할 수 있도록 합니다.

queryKey: ['myCrewJoined'],
queryFn: ({ pageParam = 0 }) =>
getMyCrewJoinedList({ page: pageParam, size, sort }).then((response) => {
if (response === undefined) {
throw new Error('크루 목록을 불러오는데 실패했습니다.');
}
return response;
}),
getNextPageParam: (lastPage: MyCrewListResponse, allPages: MyCrewListResponse[]) =>
lastPage.hasNext ? allPages.length : undefined,
};
}
23 changes: 23 additions & 0 deletions src/app/(crew)/my-crew/hosted/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useGetMyCrewHostedQuery } from '@/src/_queries/crew/my-crew-hosted-list-query';
import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll';
import CrewCardList from '@/src/components/common/crew-list/crew-card-list';

export default function MyCrewParticipationPage() {
const { data, ref, isFetchingNextPage } = useInfiniteScroll(
useGetMyCrewHostedQuery({
pageable: { page: 0, size: 6, sort: ['createdAt,desc'] },
}),
);
return (
<div>
<CrewCardList
inWhere="my-crew"
data={data ?? { pages: [], pageParams: [] }}
ref={ref}
isFetchingNextPage={isFetchingNextPage}
/>
</div>
);
}
23 changes: 23 additions & 0 deletions src/app/(crew)/my-crew/joined/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useGetMyCrewJoinedQuery } from '@/src/_queries/crew/my-crew-joined-list-query';
import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll';
import CrewCardList from '@/src/components/common/crew-list/crew-card-list';

export default function MyCrewParticipationPage() {
const { data, ref, isFetchingNextPage } = useInfiniteScroll(
useGetMyCrewJoinedQuery({
pageable: { page: 0, size: 6, sort: ['createdAt,desc'] },
}),
);
return (
<div>
<CrewCardList
inWhere="my-crew"
data={data ?? { pages: [], pageParams: [] }}
ref={ref}
isFetchingNextPage={isFetchingNextPage}
/>
</div>
);
}
41 changes: 41 additions & 0 deletions src/app/(crew)/my-crew/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { ReactNode, useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Tabs from '@/src/components/common/tab';

export default function MyCrewLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const currentPath = usePathname();
const myCrewTabs = [
{ label: '내가 참여한 크루', id: 'joined-crew', route: '/my-crew/joined' },
{ label: '내가 만든 크루', id: 'hosted-crew', route: '/my-crew/hosted' },
];
const [currentTab, setCurrentTab] = useState(myCrewTabs[0].id);

const handleTabClick = (id: string) => {
const targetRoute = myCrewTabs.find((tab) => tab.id === id)?.route;
if (targetRoute) router.push(targetRoute);
};
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 개선이 필요합니다

targetRoute가 없는 경우에 대한 처리가 누락되어 있습니다. 사용자에게 적절한 피드백을 제공하는 것이 좋습니다.

다음과 같이 수정해보세요:

 const handleTabClick = (id: string) => {
   const targetRoute = myCrewTabs.find((tab) => tab.id === id)?.route;
-  if (targetRoute) router.push(targetRoute);
+  if (targetRoute) {
+    router.push(targetRoute);
+  } else {
+    console.error(`탭 ID ${id}에 해당하는 경로를 찾을 수 없습니다.`);
+    // TODO: 사용자에게 에러 메시지 표시
+  }
 };

Committable suggestion skipped: line range outside the PR's diff.


useEffect(() => {
const activeTabId = myCrewTabs.find((tab) => tab.route === currentPath)?.id;
if (activeTabId) setCurrentTab(activeTabId);
}, [currentPath]);

return (
<div className="py-8 md:py-12.5">
<div className="px-3 md:px-8 lg:px-11.5">
<Tabs
variant="default"
tabs={myCrewTabs}
activeTab={currentTab}
onTabClick={(id) => {
handleTabClick(id);
}}
/>
</div>
<div className="mt-8 px-3 md:px-8 lg:px-11.5">{children}</div>
</div>
);
}
37 changes: 2 additions & 35 deletions src/app/(crew)/my-crew/page.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,7 @@
'use client';

import { useState } from 'react';
import Tabs from '@/src/components/common/tab';
import { redirect } from 'next/navigation';

export default function MyCrewPage() {
const myPageTabs = [
{ label: '내가 참여한 크루', id: 'joined-crew' },
{ label: '내가 만든 크루', id: 'made-crew' },
];
const [currentTab, setCurrentTab] = useState(myPageTabs[0].id);

// TODO: fetchCrewData 함수를 사용하여 데이터를 불러오기 : 파라미터 수정 필요
// TODO: 리스트와는 다른 데이터를 사용해야해서 우선 주석처리 했습니다.
// const { data, ref, isFetchingNextPage } =
// useInfiniteScroll<MyCrewListResponse>(useGetCrewListQuery());

return (
<div className="py-8 md:py-12.5">
<div className="px-3 md:px-8 lg:px-11.5">
<Tabs
variant="default"
tabs={myPageTabs}
activeTab={currentTab}
onTabClick={(id) => {
setCurrentTab(id);
}}
/>
</div>
<div className="mt-8 px-3 md:px-8 lg:px-11.5">
{/* <CrewCardList
inWhere="my-crew"
data={data}
ref={ref}
isFetchingNextPage={isFetchingNextPage}
/> */}
</div>
</div>
);
redirect('/my-crew/joined');
}
13 changes: 8 additions & 5 deletions src/app/(crew)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ export default function HomePage() {

const { data, ref, isFetchingNextPage } = useInfiniteScroll(
useGetCrewListQuery({
keyword: search,
mainLocation: handleRegionChange(region),
mainCategory,
subCategory,
sortType: sort === 'latest' ? 'LATEST' : 'POPULAR',
condition: {
keyword: search,
mainLocation: handleRegionChange(region),
mainCategory,
subCategory,
sortType: sort === 'latest' ? 'LATEST' : 'POPULAR',
},
pageable: { page: 0, size: 6, sort: ['createdAt,desc'] },
}),
);

Expand Down
1 change: 0 additions & 1 deletion src/app/_components/hero/hero-crew.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Image from 'next/image';
import Link from 'next/link';
import { useAuthStore } from '@/src/store/use-auth-store';
import Button from '@/src/components/common/input/button';
import ImgHeroCrew from '@/public/assets/icons/ic-dumbbell.svg';

export default function HeroCrew() {
Expand Down
13 changes: 8 additions & 5 deletions src/components/common/crew-list/crew-card-list.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ type Story = StoryObj<typeof meta>;
function RenderCrewCardList() {
const { data, ref, isFetchingNextPage } = useInfiniteScroll(
useGetCrewListQuery({
keyword: '',
mainLocation: '',
mainCategory: '',
subCategory: '',
sortType: 'LATEST',
condition: {
keyword: '',
mainLocation: '',
mainCategory: '',
subCategory: '',
sortType: 'LATEST',
},
pageable: { page: 0, size: 6, sort: ['createdAt,desc'] },
}),
);

Expand Down
20 changes: 6 additions & 14 deletions src/components/common/crew-list/crew-card-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,21 @@ import {
import CrewCard from './crew-card';

// CrewCardListProps 타입을 구분하여 정의
interface MainCrewCardListProps {
data: InfiniteData<MainCrewListResponse>;
interface CrewCardListProps {
data: InfiniteData<MainCrewListResponse | MyCrewListResponse>;
isFetchingNextPage: boolean;
inWhere?: undefined;
inWhere?: 'my-crew' | 'main-crew';
}

interface MyCrewCardListProps {
data: InfiniteData<MyCrewListResponse>;
isFetchingNextPage: boolean;
inWhere: 'my-crew';
}

// 유니온 타입으로 정의
type CrewCardListProps = MainCrewCardListProps | MyCrewCardListProps;

function CrewCardList(
{ data, isFetchingNextPage, inWhere }: CrewCardListProps,
ref: React.Ref<HTMLDivElement>,
) {
const crewDataList =
(inWhere === 'my-crew'
? data?.pages.flatMap((page) => page?.content)
: data?.pages?.flatMap((page) => page?.content)) ?? [];
? data?.pages.flatMap((page) => page?.content as MyCrewList[])
: data?.pages?.flatMap((page) => page?.content as MainCrewList[])) ?? [];

const gridColsStyle = inWhere === 'my-crew' ? '' : 'lg:grid-cols-2';

if (data?.pages[0] === undefined)
Expand Down
6 changes: 2 additions & 4 deletions src/components/common/crew-list/crew-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import IcoUser from '@/public/assets/icons/ic-user.svg';
import Profiles from './profiles';

interface CrewCardProps extends MainCrewList {
inWhere?: 'my-crew';
inWhere?: 'my-crew' | 'main-crew';
}

export default function CrewCard({
Expand All @@ -27,9 +27,7 @@ export default function CrewCard({
inWhere,
}: CrewCardProps) {
const [prefetched, setPrefetched] = useState(new Set());
// NOTE: api연결 후 되돌리기
// const CREWPAGE = `/crew/detail/${id}`;
const CREWPAGE = `/crew/detail/1`;
const CREWPAGE = `/crew/detail/${id}`;
const router = useRouter();

const handleCardClick = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/input/text-input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { RefObject } from 'react';
import React from 'react';
import { UseFormRegisterReturn } from 'react-hook-form';
import {
TextInput as MantineTextInput,
Expand Down