Skip to content

Conversation

@HaeJungg
Copy link
Contributor

@HaeJungg HaeJungg commented Nov 21, 2024

🔖 Issue Ticket

#159

✍️ Description

크루 리뷰 리스트 api 연결
자잘한 ui수정이 있습니다..

✅ Checklist

PR

  • Branch Convention 확인

    feat/ 피쳐, fix/ 버그 수정, refactor/ 개선

  • Base Branch 확인
  • 적절한 Label 지정
  • Assignee 및 Reviewer 지정

Test

  • 로컬 작동 확인

Summary by CodeRabbit

  • 새로운 기능
    • 크루 리뷰를 가져오는 비동기 함수 getCrewReviews 추가
    • 커스텀 훅 useGetCrewReviewsQuery 추가로 크루 리뷰를 효율적으로 가져옴
    • 로딩 상태를 표시하는 ReviewListSkeleton 컴포넌트 추가
    • ReviewCardSkeleton 컴포넌트 추가로 리뷰 카드의 로딩 UI 제공
    • location 속성이 추가된 ReviewableGatheringCard 컴포넌트 개선
  • 버그 수정
    • 리뷰 카드에서 idrefetch를 선택적으로 처리하여 오류 방지
  • UI 개선
    • CreateGatheringReviewableGatheringCard 컴포넌트의 레이아웃 및 스타일 조정
    • 반응형 디자인을 위한 스타일 조정 및 텍스트 크기 변경
    • GatheringListSection의 좋아요/싫어요 기능에 대한 성공 및 오류 메시지 개선
  • 문서화
    • ReviewListSkeleton에 대한 스토리북 설정 추가

@HaeJungg HaeJungg self-assigned this Nov 21, 2024
@HaeJungg HaeJungg linked an issue Nov 21, 2024 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2024

Walkthrough

이 PR에서는 새로운 비동기 함수 getCrewReviews와 커스텀 훅 useGetCrewReviewsQuery가 추가되었습니다. getCrewReviews는 주어진 crewIdpage를 사용하여 크루 리뷰를 가져오는 API 호출을 수행합니다. 또한, 여러 컴포넌트에서 UI와 스타일링 수정이 이루어져 반응형 디자인을 개선하고, 데이터 로딩 상태를 처리하는 방법이 업데이트되었습니다. 리뷰 데이터 구조와 타입 정의도 변경되어 리뷰 관련 정보를 보다 상세하게 관리할 수 있게 되었습니다.

Changes

파일 경로 변경 요약
src/_apis/crew/crew-review-apis.ts 새로운 비동기 함수 getCrewReviews 추가. crewIdpage를 매개변수로 받아 리뷰 데이터를 반환.
src/_queries/crew/crew-review-queries.ts 커스텀 훅 useGetCrewReviewsQuery 추가. getCrewReviews API 호출을 사용하여 리뷰 데이터를 가져옴.
src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx JSX 요소의 레이아웃과 스타일 수정. 작은 화면에서 수직 레이아웃으로 변경.
src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx ReviewCard 컴포넌트의 매핑 주석 제거, Pagination 컴포넌트의 클래스 이름 업데이트.
src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx totalReviewCount 속성을 totalRate로 변경. 기본 점수 데이터 구조 추가.
src/app/(crew)/crew/detail/[id]/_components/review-section.tsx crewId prop 추가, useGetCrewReviewsQuery 훅 사용하여 리뷰 데이터 가져오기.
src/app/(crew)/crew/detail/[id]/page.tsx 여러 컴포넌트를 동적 import로 변경하고 crewId prop 전달.
src/app/(crew)/my-page/my-review/page.tsx ReviewListSkeleton을 사용하여 로딩 상태 표시.
src/components/common/crew-list/crew-card.tsx 상태 변수 prefetchedprefetchedPages로 이름 변경, 마우스 이벤트 처리 로직 수정.
src/components/common/review-list/review-card.tsx idrefetch 속성을 선택적으로 변경, 삭제 처리 로직 개선.
src/components/common/skeleton/my-review-skeleton-list/index.tsx 매개변수 없이 항상 3개의 MyReviewSkeleton 컴포넌트를 렌더링하도록 변경.
src/components/common/skeleton/my-review-skeleton/index.tsx 레이아웃과 스타일 수정, 반응형 디자인 개선.
src/components/common/skeleton/review-skeleton/index.tsx 새로운 ReviewListSkeleton 컴포넌트 추가, type prop에 따라 다른 스켈레톤 구조 렌더링.
src/components/common/skeleton/review-skeleton/review-card.tsx 새로운 ReviewCardSkeleton 컴포넌트 추가, isMine prop에 따라 다른 스켈레톤 구조 렌더링.
src/components/gathering-list/liked-list-presenter.tsx gatheringData에서 pageSize 속성 제거.
src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx 로딩 상태 추가, location 속성 추가하여 ReviewableGatheringCard에 전달.
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx ReviewableGatheringCard에 대한 새로운 Storybook 구성 추가.
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx location 속성 추가, 레이아웃 및 스타일 수정.
src/mock/review-data.ts imageUrl 속성을 profileImageUrl로 변경.
src/types/review.d.ts 여러 인터페이스 업데이트 및 추가, 리뷰 관련 타입 정의 확장.

Suggested reviewers

  • yulrang
  • minkyung5x5
  • Rangbyeolang

🐇 변화의 바람이 불어와
크루 리뷰가 새롭게 태어나네
비동기로 데이터를 가져오고
스켈레톤으로 기다림을 덜어내네
반응형 디자인으로 모두가 행복해
리뷰의 세계가 더욱 빛나리라! ✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint (1.23.1)

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/app/(crew)/my-page/_components/profile-card/container.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't determine the plugin "react-hooks" uniquely.

  • /node_modules/eslint-plugin-react-hooks/index.js (loaded in ".eslintrc.js")
  • /node_modules/eslint-config-next/node_modules/eslint-plugin-react-hooks/index.js (loaded in ".eslintrc.js » eslint-config-next/core-web-vitals » /node_modules/eslint-config-next/index.js » plugin:react-hooks/recommended")

Please remove the "plugins" setting from either config or remove either plugin installation.

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c97c7bf and 5a89fe7.

📒 Files selected for processing (1)
  • src/app/(crew)/my-page/_components/profile-card/container.tsx (2 hunks)
🔇 Additional comments (3)
src/app/(crew)/my-page/_components/profile-card/container.tsx (3)

18-18: 상태 관리 개선이 잘 이루어졌습니다.

불필요한 상태 변수를 제거하고 user 객체에서 직접 profileImageUrl을 도출하도록 변경한 것이 좋은 개선입니다.


20-23: 로딩 상태와 null 체크 처리가 명확해졌습니다.

로딩 상태와 사용자 데이터 부재 시의 처리가 명확하게 구분되어 있어 좋습니다. 주석도 이해하기 쉽게 작성되었습니다.


41-41: ⚠️ Potential issue

메모리 누수 방지를 위한 URL 정리가 필요합니다.

임시 URL을 생성하는 경우 메모리 누수를 방지하기 위해 정리가 필요합니다.

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

   const file = (event.target as HTMLInputElement)?.files?.[0];
   if (file) {
     if (file.size > 5 * 1024 * 1024) {
       toast.error('5MB 이하의 파일만 업로드 가능합니다.');
       return;
     }

     try {
+      const tempUrl = URL.createObjectURL(file);
       await updateUserProfile(file);
       toast.success('프로필 이미지가 업데이트되었습니다.');
       await refetchUser();
+      URL.revokeObjectURL(tempUrl);
     } catch (error) {
       toast.error('파일 업로드에 실패했습니다.');
     }
   }

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (25)
src/components/common/skeleton/my-review-skeleton-list/index.tsx (1)

6-6: Array.from 사용에 대한 제안

현재 구현은 작동하지만, 더 간단한 방법을 제안드립니다.

다음과 같이 변경하는 것을 고려해보세요:

-{Array.from({ length: 3 }).map((_, idx) => (
+{[...Array(3)].map((_, idx) => (
src/_apis/crew/crew-review-apis.ts (1)

4-16: API 구현에 대한 개선 제안

다음 사항들을 고려해 주시기 바랍니다:

  1. 페이지 크기(size=5)가 하드코딩되어 있습니다. 이를 매개변수로 받거나 설정 파일로 관리하는 것이 더 유연할 것 같습니다.
  2. API 응답 실패에 대한 구체적인 에러 처리가 필요해 보입니다.

다음과 같이 개선해 보시는 것은 어떨까요?:

-export async function getCrewReviews(crewId: number, page: number): Promise<CrewReviewResponse> {
-  const url = `/api/review/${crewId}?page=${page}&size=5`;
+export async function getCrewReviews(
+  crewId: number,
+  page: number,
+  pageSize: number = 5
+): Promise<CrewReviewResponse> {
+  const url = `/api/review/${crewId}?page=${page}&size=${pageSize}`;

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

+  if (!response.data) {
+    throw new Error('크루 리뷰 데이터를 불러오는데 실패했습니다.');
+  }

   return response.data;
 }
src/components/common/skeleton/my-review-skeleton/index.tsx (1)

6-6: 스켈레톤 이미지에 로딩 애니메이션 추가를 고려해보세요.

이미지 스켈레톤의 반응형 구현은 잘 되어있지만, 사용자 경험 향상을 위해 펄스 애니메이션을 추가하면 좋을 것 같습니다.

-<Skeleton className="relative h-28 w-28 flex-shrink-0 rounded-[12px] md:h-[166px] md:w-[294px]" />
+<Skeleton className="relative h-28 w-28 flex-shrink-0 rounded-[12px] md:h-[166px] md:w-[294px] animate-pulse" />
src/components/common/skeleton/review-skeleton/index.tsx (3)

12-17: 배열 인덱스를 key로 사용하는 것에 대한 대안을 고려해보세요.

현재 구현에서는 ESLint 규칙을 비활성화하고 배열 인덱스를 key로 사용하고 있습니다. 스켈레톤 컴포넌트의 특성상 큰 문제는 없지만, 더 나은 방법을 제안드립니다.

다음과 같이 수정하는 것을 고려해보세요:

-          {Array.from({ length: 3 }).map((_, idx) => (
-            // eslint-disable-next-line react/no-array-index-key
-            <li key={`mine-skeleton-${idx}`} className="h-auto min-h-[112px]">
+          {['skeleton1', 'skeleton2', 'skeleton3'].map((id) => (
+            <li key={id} className="h-auto min-h-[112px]">

21-26: 그리드 레이아웃의 반응형 처리가 필요할 수 있습니다.

현재 그리드 레이아웃이 고정된 갭으로 구현되어 있습니다. 다양한 화면 크기에 대응하기 위해 반응형 처리를 추가하는 것이 좋을 것 같습니다.

다음과 같이 수정하는 것을 고려해보세요:

-        <div className="mb-6 grid flex-grow gap-4">
+        <div className="mb-6 grid flex-grow gap-4 sm:gap-6 md:gap-8">

7-30: 컴포넌트 구조가 잘 구성되어 있습니다.

전반적으로 컴포넌트의 구조가 깔끔하고, 조건부 렌더링을 통해 타입별로 적절한 스켈레톤을 표시하고 있습니다. 재사용성과 유지보수성이 좋아 보입니다.

추후 스켈레톤의 개수를 props로 받아 더 유연하게 처리하는 것을 고려해보세요.

src/app/(crew)/my-page/my-review/page.tsx (1)

19-22: 로딩 상태 UI가 개선되었으나, 구조를 더 단순화할 수 있습니다.

스켈레톤 UI를 사용하여 사용자 경험이 향상되었습니다. 하지만 중첩된 div 구조를 단순화할 수 있습니다.

다음과 같이 단일 div로 구조를 단순화하는 것을 제안드립니다:

-      <div className="mt-8">
-        <div className="w-full">
-          <ReviewListSkeleton type="mine" />
-        </div>
-      </div>
+      <div className="mt-8 w-full">
+        <ReviewListSkeleton type="mine" />
+      </div>
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx (1)

41-45: 참가자 프로필 이미지 테스트 데이터 보완이 필요합니다.

현재 모든 참가자의 profileImageUrl이 비어있어 컴포넌트의 이미지 처리 기능을 제대로 테스트할 수 없습니다.

최소한 한 명의 참가자에게는 실제 이미지 URL을 설정하여 이미지 렌더링을 테스트하는 것을 추천드립니다:

     participants: [
-      { id: 1, nickname: '유저1', profileImageUrl: '' },
+      { id: 1, nickname: '유저1', profileImageUrl: 'https://example.com/profile1.jpg' },
       { id: 2, nickname: '유저2', profileImageUrl: '' },
       { id: 3, nickname: '유저3', profileImageUrl: '' },
     ],
src/components/common/skeleton/review-skeleton/review-card.tsx (2)

7-16: 조건부 스타일링 로직 개선이 필요합니다.

템플릿 리터럴 내의 조건부 클래스네임이 복잡해 보입니다. 가독성 향상을 위해 별도의 변수로 추출하는 것이 좋겠습니다.

다음과 같이 개선해보세요:

+  const containerClassName = isMine
+    ? 'rounded-[12px] p-6 shadow-bg'
+    : 'border-b-[2px] border-[#F3F4F6] py-4';
+
   <div
     role="presentation"
-    className={`flex h-full items-end gap-[15px] ${
-      isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4'
-    } bg-white lg:gap-[40px]`}
+    className={`flex h-full items-end gap-[15px] ${containerClassName} bg-white lg:gap-[40px]`}
   >

26-34: 하단 영역의 조건부 렌더링 최적화가 필요합니다.

현재 구조에서는 불필요한 중첩된 삼항 연산자와 조건부 렌더링이 있습니다. 이를 더 명확하게 분리하면 좋겠습니다.

다음과 같이 개선해보세요:

-  <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}>
-    {!isMine && (
-      <>
-        <Skeleton className="h-8 w-8 rounded-full" />
-        <Skeleton className="h-4 w-24" />
-      </>
-    )}
-    <Skeleton className="h-4 w-12" />
-  </div>
+  <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}>
+    {isMine ? (
+      <Skeleton className="h-4 w-12" />
+    ) : (
+      <div className="flex items-center gap-2">
+        <Skeleton className="h-8 w-8 rounded-full" />
+        <Skeleton className="h-4 w-24" />
+        <Skeleton className="h-4 w-12" />
+      </div>
+    )}
+  </div>
src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx (1)

Line range hint 25-34: 리뷰 목록이 비어있을 때의 상태 처리가 필요합니다.

현재 구현에서는 리뷰 목록이 비어있을 때의 UI 처리가 누락되어 있습니다. 사용자 경험 향상을 위해 빈 상태 처리를 추가하는 것이 좋겠습니다.

다음과 같은 방식으로 개선해보세요:

 <div className="mb-6 grid flex-grow gap-4">
+  {reviews.length === 0 ? (
+    <div className="text-center text-gray-500">
+      아직 작성된 리뷰가 없습니다.
+    </div>
+  ) : (
     {reviews.map((review) => (
       <ReviewCard
         key={review.id}
         rate={review.rate}
         comment={review.comment}
         createdAt={review.createdAt}
         crewId={review.crewId}
         reviewer={review.reviewer}
       />
     ))}
+  )}
 </div>
src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx (1)

Line range hint 9-13: useInfiniteScroll 훅 사용이 개선되었습니다.

isLoading 상태값이 적절히 추가되었습니다. 다만, 타입 안정성을 위해 다음과 같은 개선을 제안드립니다.

- const { data, ref, isFetchingNextPage, isLoading } = useInfiniteScroll(
+ const { 
+   data, 
+   ref, 
+   isFetchingNextPage, 
+   isLoading 
+ }: {
+   data: ReviewableGatheringResponse | undefined;
+   ref: RefCallback<HTMLDivElement>;
+   isFetchingNextPage: boolean;
+   isLoading: boolean;
+ } = useInfiniteScroll(
src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx (1)

42-46: 반응형 디자인 개선이 잘 이루어졌습니다!

모바일과 데스크톱 환경 모두에서 최적화된 레이아웃을 제공하도록 구현되었습니다.

접근성 개선을 위한 제안

스크린 리더 사용자를 위해 약속 개수 정보에 aria-label을 추가하면 좋을 것 같습니다.

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

-        <span className="text-sm font-semibold text-blue-500 md:text-base">
+        <span 
+          className="text-sm font-semibold text-blue-500 md:text-base"
+          aria-label={`현재 개설된 약속 수: ${totalGatheringCount}개`}
+        >
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx (2)

43-53: 이미지 최적화를 위한 제안사항이 있습니다.

이미지 처리가 전반적으로 잘 구현되어 있으나, 성능 최적화를 위해 다음과 같은 개선을 고려해보세요:

 <Image
   src={imageUrl}
   alt={gatheringName}
   fill
   sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw"
+  loading="lazy"
+  quality={75}
   className="h-full w-full object-cover"
 />

Line range hint 22-102: 에러 처리 개선을 위한 제안사항입니다.

컴포넌트 구조는 잘 되어있으나, 에러 처리를 위한 ErrorBoundary 추가를 고려해보세요:

// ErrorBoundary 컴포넌트 예시
class ReviewableGatheringCardErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <div>리뷰 카드를 불러오는  문제가 발생했습니다.</div>;
    }
    return this.props.children;
  }
}

// 사용 예시:
<ReviewableGatheringCardErrorBoundary>
  <ReviewableGatheringCard {...props} />
</ReviewableGatheringCardErrorBoundary>
src/components/common/crew-list/crew-card.tsx (2)

37-41: 성능 최적화를 위한 Set 업데이트 방식 개선이 필요합니다.

현재 구현은 매번 새로운 Set을 생성하고 있습니다. 다음과 같이 기존 Set을 직접 수정하는 방식으로 개선할 수 있습니다.

  const handleMouseEnter = () => {
    if (!prefetchedPages.has(CREWPAGE)) {
      router.prefetch(CREWPAGE);
-     setPrefetchedPages(new Set(prefetchedPages).add(CREWPAGE));
+     setPrefetchedPages(prev => {
+       prev.add(CREWPAGE);
+       return new Set(prev);
+     });
    }
  };

37-41: 프리패칭 전략에 대한 제안

현재 마우스 진입 시에만 프리패칭을 수행하고 있습니다. 다음과 같은 개선을 고려해보세요:

  1. Intersection Observer를 사용하여 카드가 뷰포트에 진입할 때 프리패칭
  2. 사용자의 스크롤 속도와 방향을 고려한 예측적 프리패칭
  3. 네트워크 상태에 따른 조건부 프리패칭

이러한 전략은 사용자 경험을 더욱 개선할 수 있습니다.

src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx (1)

54-64: 데이터 처리 로직이 잘 구현되었습니다

기본 점수 데이터 구조와 병합 로직이 깔끔하게 구현되었습니다. 다만, 성능 최적화를 위해 다음과 같은 개선사항을 고려해보시면 좋을 것 같습니다.

-  const defaultRatingsData = Array.from({ length: 5 }, (_, index) => ({
-    score: 5 - index,
-    count: 0,
-  }));
+  // 컴포넌트 외부 또는 useMemo로 캐싱하여 재렌더링 시 불필요한 재생성 방지
+  const defaultRatingsData = useMemo(
+    () =>
+      Array.from({ length: 5 }, (_, index) => ({
+        score: 5 - index,
+        count: 0,
+      })),
+    []
+  );
src/components/common/review-list/review-card.tsx (2)

90-94: 스타일링 개선사항이 있습니다.

cn 유틸리티를 사용하여 조건부 클래스 적용이 더 명확해졌습니다. 다만, 매직 넘버(12px, 40px 등)를 사용하고 있어 개선이 필요합니다.

-          'rounded-[12px] p-6 shadow-bg'
+          'rounded-xl p-6 shadow-bg'
-          'lg:gap-[40px]'
+          'lg:gap-10'

121-121: 버튼 스타일링이 개선되었습니다.

반응형 디자인을 고려한 패딩 조정이 적절히 이루어졌습니다. 다만, 매직 넘버 사용을 피하고 Tailwind의 기본 스페이싱을 활용하는 것이 좋습니다.

-            className="btn-outlined absolute bottom-4 right-4 flex-shrink-0 p-[4px_10px] text-base font-semibold md:bottom-6 md:right-6 md:px-4"
+            className="btn-outlined absolute bottom-4 right-4 flex-shrink-0 px-2.5 py-1 text-base font-semibold md:bottom-6 md:right-6 md:px-4"
src/mock/review-data.ts (5)

Line range hint 69-71: 중복된 ID 값이 발견되었습니다

여러 리뷰 데이터에서 ID 값 5가 중복되어 사용되고 있습니다. 고유한 식별자로서 ID는 중복되지 않아야 합니다.

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

-      id: 5,
+      id: 6,
       rate: 1,
// ... 그리고
-      id: 5,
+      id: 7,
       rate: 4,

Also applies to: 82-84, 95-97


Line range hint 254-254: 날짜 형식이 일관되지 않습니다

createdAt 필드의 날짜 형식이 일관되지 않습니다. 대부분 ISO 8601 형식(YYYY-MM-DDThh:mm:ss.sss+HH:MM)을 사용하고 있으나, 일부는 다른 형식(YYYY-MM-DDThh:mm)을 사용하고 있습니다.

모든 날짜를 ISO 8601 형식으로 통일해주세요:

-          createdAt: '2024-10-31T00:56',
+          createdAt: '2024-10-31T00:56:00.000+09:00',

Also applies to: 276-276


18-18: 프로필 이미지 URL이 모두 동일합니다

모든 리뷰어의 profileImageUrl이 동일한 값을 사용하고 있습니다. 목업 데이터의 현실성을 높이기 위해 다양한 이미지 URL을 사용하는 것이 좋습니다.

파일 상단에 주석 처리된 이미지 URL들을 활용하여 다양한 프로필 이미지를 설정해주세요.

Also applies to: 30-30, 42-42, 54-54, 66-66, 78-78, 90-90, 102-102, 114-114, 126-126, 138-138, 150-150, 162-162, 174-174, 186-186, 198-198, 210-210


Line range hint 1-3: 주석 처리된 이미지 URL 정리가 필요합니다

현재 사용되지 않는 이미지 URL들이 주석으로 남아있습니다. 코드의 가독성을 위해 사용하지 않는 주석은 제거하거나, 실제 데이터에서 활용하는 것이 좋습니다.


Line range hint 219-297: 페이지네이션 테스트를 위한 데이터 보완이 필요합니다

MyReviewData의 페이지네이션 테스트 데이터에서 다음과 같은 부분을 보완하면 좋겠습니다:

  1. pageParams 배열이 비어있습니다
  2. 각 페이지의 데이터 수가 일정합니다

테스트의 완성도를 높이기 위해 다음 사항을 고려해주세요:

  1. 실제 페이지네이션 파라미터 예시 추가
  2. 페이지별로 다양한 수의 데이터 포함
  3. 경계값 테스트를 위한 데이터 추가 (빈 페이지, 최대 개수 등)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 59eaf95 and 0945503.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • src/_apis/crew/crew-review-apis.ts (1 hunks)
  • src/_queries/crew/crew-review-queries.ts (1 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx (1 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx (3 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx (4 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/review-section.tsx (1 hunks)
  • src/app/(crew)/crew/detail/[id]/page.tsx (2 hunks)
  • src/app/(crew)/my-page/my-review/page.tsx (2 hunks)
  • src/components/common/crew-list/crew-card.tsx (1 hunks)
  • src/components/common/review-list/review-card.tsx (5 hunks)
  • src/components/common/skeleton/my-review-skeleton-list/index.tsx (1 hunks)
  • src/components/common/skeleton/my-review-skeleton/index.tsx (1 hunks)
  • src/components/common/skeleton/review-skeleton/index.tsx (1 hunks)
  • src/components/common/skeleton/review-skeleton/review-card.tsx (1 hunks)
  • src/components/gathering-list/liked-list-presenter.tsx (1 hunks)
  • src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx (3 hunks)
  • src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx (1 hunks)
  • src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx (3 hunks)
  • src/mock/review-data.ts (17 hunks)
  • src/types/review.d.ts (1 hunks)
🔇 Additional comments (42)
src/_queries/crew/crew-review-queries.ts (2)

1-2: LGTM! 필요한 의존성들이 올바르게 임포트되었습니다.

필수 라이브러리와 API 함수가 적절하게 임포트되었습니다.


4-9: API 호출 검증이 필요합니다.

크루 리뷰 API가 올바르게 호출되는지 확인이 필요합니다.

다음 스크립트로 API 호출 패턴을 검증해보겠습니다:

✅ Verification successful

API 호출이 올바르게 구현되어 있습니다.

크루 리뷰 API 구현을 검증한 결과:

  • getCrewReviews 함수가 src/_apis/crew/crew-review-apis.ts에 올바르게 구현되어 있습니다
  • API 엔드포인트, HTTP 메서드, 헤더 설정이 적절합니다
  • useGetCrewReviewsQuery에서 페이지네이션 처리를 위해 page - 1로 변환하여 호출하는 것이 확인되었습니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# API 구현 확인
echo "크루 리뷰 API 구현 확인:"
ast-grep --pattern 'export async function getCrewReviews($_: number, $_: number)'

# API 응답 타입 정의 확인
echo "응답 타입 정의 확인:"
rg -A 5 "type CrewReviewResponse"

Length of output: 197


Script:

#!/bin/bash
# API 구현 파일 찾기
echo "API 구현 파일 찾기:"
fd "crew.*api" --type f

# getCrewReviews 함수 검색
echo -e "\ngetCrewReviews 함수 검색:"
rg "getCrewReviews" -A 5

# 크루 리뷰 관련 타입 정의 검색
echo -e "\n크루 리뷰 타입 정의 검색:"
rg "Review.*type|type.*Review" -A 3

Length of output: 9736


Script:

#!/bin/bash
# 리뷰 타입 정의 확인
echo "리뷰 타입 정의 확인:"
rg -p "type CrewReview|type CrewReviewResponse" src/types/review.ts -A 5

# API 구현 상세 확인
echo -e "\nAPI 구현 상세 확인:"
cat src/_apis/crew/crew-review-apis.ts

Length of output: 712

src/components/common/skeleton/my-review-skeleton-list/index.tsx (3)

5-5: 시맨틱 마크업으로의 개선을 승인합니다

div에서 ul/li 구조로 변경한 것은 웹 접근성과 시맨틱 마크업 측면에서 적절한 개선입니다. 또한 flex layout과 gap을 활용한 스타일링도 잘 구현되었습니다.

Also applies to: 8-10


8-8: ESLint 규칙 비활성화에 대한 의견

index를 key로 사용하는 것에 대한 ESLint 경고를 비활성화하셨는데, 이 경우는 고정된 개수의 정적 리스트이므로 문제가 되지 않습니다. 주석은 유지하는 것이 좋겠습니다.


3-3: props 제거로 인한 컴포넌트 재사용성 검토 필요

동적인 skeleton 개수 설정이 불가능해졌습니다. 다른 페이지나 컴포넌트에서 다른 개수의 skeleton이 필요한 경우를 고려하여, props를 유지하는 것이 더 좋을 수 있습니다.

다음 스크립트로 다른 페이지에서의 사용 사례를 확인해보겠습니다:

✅ Verification successful

고정된 스켈레톤 개수는 현재 사용 사례에 적합합니다

현재 MyReviewSkeletonList는 로딩 상태를 표시하는 용도로만 사용되고 있으며, 다른 페이지나 컴포넌트에서 재사용되지 않습니다. 3개의 고정된 스켈레톤 아이템은 현재 구현된 사용 사례에 충분합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check usage of MyReviewSkeletonList in other components
rg -l "MyReviewSkeletonList"

Length of output: 173


Script:

#!/bin/bash
# Check how MyReviewSkeletonList is used in reviewable-gathering-card-list.tsx
rg -A 5 -B 5 "MyReviewSkeletonList" src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx

Length of output: 909

src/_apis/crew/crew-review-apis.ts (1)

1-2: 타입과 유틸리티 임포트가 적절히 구성되어 있습니다.

필요한 유틸리티와 타입이 명확하게 임포트되어 있으며, 타입 정의가 잘 되어 있습니다.

src/components/common/skeleton/my-review-skeleton/index.tsx (1)

5-5: 컨테이너 레이아웃이 적절히 구현되었습니다.

반응형 레이아웃을 위한 flex-wrap과 최대 너비 제한이 잘 설정되어 있습니다. 경계선과 패딩도 적절히 적용되었습니다.

src/components/common/skeleton/review-skeleton/index.tsx (1)

3-5: 타입 정의가 명확하고 적절합니다.

인터페이스가 간단하고 목적에 맞게 잘 정의되어 있습니다.

src/app/(crew)/my-page/my-review/page.tsx (1)

8-8: 스켈레톤 컴포넌트 임포트가 적절합니다!

컴포넌트 이름이 명확하고 절대 경로 임포트 패턴을 잘 따르고 있습니다.

src/types/review.d.ts (3)

32-39: 리뷰 평점 정보 인터페이스가 잘 구성되었습니다

평점의 총합, 평균, 그리고 점수별 분포를 명확하게 표현하고 있습니다.


49-51: 응답 타입이 깔끔하게 구성되었습니다

리뷰 평점 정보와 리뷰 목록을 하나의 응답으로 잘 구조화했습니다.


29-30: 프로퍼티 이름 변경에 따른 사용처 확인 필요

imageUrl에서 profileImageUrl로의 변경이 다른 컴포넌트에도 영향을 미칠 수 있습니다.

✅ Verification successful

Based on the search results, I can generate the final response as the verification is complete. The search shows that:

  1. The property imageUrl is used in different contexts (crew images, gathering images) and is not related to profile images
  2. The property profileImageUrl is consistently used across the codebase for user profile images in relevant interfaces and components
  3. The change from imageUrl to profileImageUrl in src/types/review.d.ts aligns with the existing usage pattern for profile images

프로퍼티 이름 변경이 올바르게 적용되어 있습니다

imageUrl에서 profileImageUrl로의 변경이 코드베이스 전체에서 일관되게 적용되어 있습니다. 사용자 프로필 이미지는 profileImageUrl을, 다른 이미지(모임, 크루 등)는 imageUrl을 사용하여 명확하게 구분되어 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining usage of old property name

# Search for imageUrl usage in TypeScript/JavaScript files
rg "imageUrl" --type ts --type js

# Search for profileImageUrl usage to confirm the change
rg "profileImageUrl" --type ts --type js

Length of output: 24414

src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx (1)

1-28: 스토리북 설정이 잘 구성되어 있습니다!

컴포넌트 중앙 정렬을 위한 데코레이터와 레이아웃 파라미터가 적절하게 설정되어 있습니다.

src/components/common/skeleton/review-skeleton/review-card.tsx (2)

1-5: Props 인터페이스가 잘 정의되어 있습니다.

isMine prop의 선택적 특성이 잘 표현되어 있으며, 타입 정의가 명확합니다.


17-25: 컨텐츠 영역의 구조가 잘 구성되어 있습니다.

스켈레톤 UI의 각 요소들이 실제 데이터를 표현하기에 적절한 크기와 간격으로 설정되어 있습니다.

src/app/(crew)/crew/detail/[id]/page.tsx (3)

40-40: 마진 조정이 적절해 보입니다

반응형 디자인을 유지하면서 상단 여백을 조정한 것이 레이아웃 개선에 도움이 될 것 같습니다.


4-9: SSR 비활성화에 대한 검토가 필요합니다

세 컴포넌트 모두 SSR을 비활성화했는데, 이는 초기 렌더링과 SEO에 영향을 미칠 수 있습니다. 각 컴포넌트가 정말로 클라이언트 사이드 렌더링만 필요한지 확인해주세요.

✅ Verification successful

클라이언트 사이드 렌더링이 필요한 것으로 확인되었습니다

검증 결과, 다음과 같은 클라이언트 종속성이 발견되어 SSR 비활성화가 적절한 것으로 판단됩니다:

  • window.location API 사용 (gathering-list-section.tsx, detail-crew-container.tsx)
  • React의 상태 관리 훅 (useState) 및 사이드 이펙트 (useEffect) 광범위하게 사용
  • 다수의 사용자 상호작용 이벤트 핸들러 존재

이러한 기능들은 브라우저 환경에서만 정상적으로 동작하므로, 현재의 SSR 비활성화 설정이 타당합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if components use browser-specific APIs that require client-side rendering

# Search for browser-specific API usage in components
echo "Checking for browser API usage in components..."
rg -t tsx "window\.|document\.|localStorage|sessionStorage" ./_components/create-gathering.tsx ./_components/gathering-list-section.tsx ./_components/review-section.tsx

Length of output: 303


Script:

#!/bin/bash
# Let's try searching in the correct directory and without file type restriction
echo "Checking for browser API usage in components..."
rg "window\.|document\.|localStorage|sessionStorage" "src/app/(crew)/crew/detail/[id]/_components/"

# Also check for React hooks that might require client-side rendering
echo -e "\nChecking for React hooks usage..."
rg "use(State|Effect|Context|Ref|Callback|Memo|Reducer)" "src/app/(crew)/crew/detail/[id]/_components/"

# Check for event handlers that might require client-side rendering
echo -e "\nChecking for event handlers..."
rg "on(Click|Change|Submit|Input)" "src/app/(crew)/crew/detail/[id]/_components/"

Length of output: 7381


43-43: CrewReviewSection 컴포넌트의 타입 안전성 확인이 필요합니다

crewId prop이 추가되었습니다. 리뷰 API 연동을 위한 적절한 변경이지만, 타입 정의가 제대로 되어있는지 확인해주세요.

src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx (1)

5-5: 클래스명 관리를 위한 cn 유틸리티 추가가 적절합니다!

클래스명을 조건부로 관리하기 위한 cn 유틸리티의 도입은 코드의 가독성과 유지보수성을 향상시킵니다.

src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx (3)

5-5: 스켈레톤 컴포넌트 import가 적절히 추가되었습니다.

로딩 상태를 시각적으로 표시하기 위한 스켈레톤 컴포넌트 import가 잘 구현되었습니다.


20-24: 로딩 상태 처리가 깔끔하게 구현되었습니다.

초기 로딩 상태를 스켈레톤 UI로 처리하여 사용자 경험이 향상되었습니다. 한글 주석 처리도 잘 되어있습니다.


39-39: location prop 타입 검증이 필요합니다.

ReviewableGatheringCard 컴포넌트에 location prop이 추가되었습니다. 관련 타입이 올바르게 정의되어 있는지 확인이 필요합니다.

✅ Verification successful

location prop이 올바르게 정의되어 있음을 확인했습니다.

ReviewableGatheringCardProps 인터페이스에 location prop이 string 타입으로 정확하게 정의되어 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# ReviewableGatheringCard의 props 타입 정의 확인
ast-grep --pattern 'interface ReviewableGatheringCardProps {
  $$$
  location: $_
  $$$
}'

Length of output: 1095

src/components/gathering-list/liked-list-presenter.tsx (2)

Line range hint 24-61: UI 구현이 적절해 보입니다.

  • 반응형 그리드 레이아웃이 잘 구현되어 있습니다
  • 페이지네이션 스타일링이 깔끔하게 처리되어 있습니다
  • GatheringCard 컴포넌트와의 연동이 잘 되어 있습니다

21-21: pageSize 제거가 적절한지 확인이 필요합니다.

pageSize prop이 제거되었는데, 이 변경이 의도적인 것인지, 그리고 페이지네이션 로직에 영향을 미치지 않는지 확인해주시기 바랍니다.

✅ Verification successful

pageSize 제거는 안전합니다

liked-list-presenter.tsx 컴포넌트에서 pageSize를 사용하지 않고 있으며, 페이지네이션 로직에도 영향을 미치지 않습니다. pageSize는 GatheringResponseType 인터페이스에 정의되어 있지만, 실제 컴포넌트에서는 content와 totalPages만 필요로 하므로 제거가 적절합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: pageSize 사용 여부 확인

# pageSize가 다른 컴포넌트에서 사용되는지 확인
rg -A 3 "pageSize.*GatheringResponseType" 

# 페이지네이션 관련 로직에서 pageSize 사용 확인
ast-grep --pattern 'interface GatheringResponseType {
  $$$
  pageSize
  $$$
}'

Length of output: 523


Script:

#!/bin/bash
# Check if pageSize is used in the component or related files
rg -A 5 "pageSize" src/components/gathering-list/

# Check pagination related logic in the component
ast-grep --pattern 'const { $$$, pageSize, $$$ } = $_'

# Check if there are any pagination components that might use pageSize
fd -e tsx -e ts pagination src/components/

Length of output: 1939

src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx (1)

15-15: 위치 정보 추가가 잘 되었습니다!

인터페이스에 location 속성이 명확하게 정의되어 있습니다.

src/components/common/crew-list/crew-card.tsx (2)

29-29: 상태 변수 이름 변경이 적절합니다!

prefetched에서 prefetchedPages로의 이름 변경은 상태의 목적을 더 명확하게 표현하며, Set 자료구조의 사용은 중복 없이 프리패치된 페이지를 관리하는데 적합합니다.


47-48: 이벤트 핸들러 바인딩이 깔끔합니다!

onClickonMouseEnter 이벤트 핸들러가 적절히 분리되어 있고, 각각의 역할이 명확합니다. 코드의 가독성과 유지보수성이 향상되었습니다.

src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx (1)

70-70: UI 렌더링 변경사항이 적절합니다

평점 표시와 프로그레스 바의 계산 로직이 일관성 있게 구현되었습니다. 특히 다음 사항들이 잘 처리되었습니다:

  • 총 평가 개수 표시
  • 프로그레스 바의 비율 계산
  • 평점 데이터 표시 방식

Also applies to: 80-80, 84-84, 88-88

src/components/common/review-list/review-card.tsx (4)

50-54: 에러 처리 로직이 개선되었습니다.

리뷰 ID가 없는 경우에 대한 early return 처리가 추가되어 안정성이 향상되었습니다.


59-59: 선택적 체이닝 연산자 사용이 적절합니다.

refetch?.() 구문을 사용하여 refetch 함수가 없는 경우도 안전하게 처리되도록 개선되었습니다.


109-109: 프로필 이미지 속성명 변경을 확인해주세요.

imageUrl에서 profileImageUrl로 변경된 부분이 API 응답 구조와 일치하는지 확인이 필요합니다.

✅ Verification successful

Let me analyze the search results to verify the profile image property change. I can see that:

  1. In the types files:
  • src/types/user.d.ts and src/types/review.d.ts use profileImageUrl
  • The reviewer-related types consistently use profileImageUrl
  1. In the mock data:
  • src/mock/review-data.ts uses profileImageUrl for reviewer data
  • Other mock data consistently uses profileImageUrl for user profiles
  1. In the components:
  • The Profile component (src/components/common/profile/index.tsx) accepts imageUrl as a prop
  • The review card component is using profileImageUrl from the reviewer object

Based on this analysis, I can now provide the final response:

프로필 이미지 속성명이 올바르게 변경되었습니다.

Profile 컴포넌트는 imageUrl prop을 받도록 설계되어 있으며, reviewer 객체에서 profileImageUrl을 사용하는 것이 API 응답 구조와 일치합니다. 타입 정의와 실제 구현이 모두 일관성 있게 적용되어 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# ReviewerType 인터페이스와 API 응답 구조 확인
rg "type ReviewerType" -A 5
rg "profileImageUrl|imageUrl" --type ts

Length of output: 23439


20-20: 선택적 프로퍼티 변경에 대한 검증이 필요합니다.

idrefetch 프로퍼티를 선택적으로 변경한 것이 다른 컴포넌트에 영향을 미치지 않는지 확인이 필요합니다.

Also applies to: 26-26

✅ Verification successful

Let me analyze the usage of ReviewCard to verify the impact of optional properties. I need to check the actual props being passed in these locations.


Let me check one more thing to verify the type definitions and usage patterns.


선택적 프로퍼티 변경이 안전하게 이루어졌습니다.

idrefetch 프로퍼티를 선택적으로 변경한 것이 적절합니다. 코드베이스 분석 결과:

  • review-card-list.tsx에서는 모든 필수 props를 올바르게 전달하고 있습니다.
  • 선택적 프로퍼티들은 컴포넌트의 조건부 기능을 위해 사용되며, 필수 기능에는 영향을 주지 않습니다.
  • 인터페이스 정의가 명확하고 일관성 있게 구현되어 있습니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# ReviewCard 컴포넌트의 사용처를 검색하여 영향도 분석
rg -A 2 "<ReviewCard" 

Length of output: 1587


Script:

#!/bin/bash
# Check implementation of ReviewCard in crew-review-list.tsx
rg -A 10 "<ReviewCard" "src/app/(crew)/crew/detail/\[id\]/_components/crew-review-list.tsx"

# Check implementation in review-card-list.tsx
rg -A 10 "<ReviewCard" "src/components/common/review-list/review-card-list.tsx"

Length of output: 632


Script:

#!/bin/bash
# Check ReviewCard props interface/type definition
ast-grep --pattern 'interface ReviewCardProps {
  $$$
}'

# Also check for type definition
ast-grep --pattern 'type ReviewCardProps = {
  $$$
}'

Length of output: 1064

src/app/(crew)/crew/detail/[id]/_components/review-section.tsx (10)

3-5: 필요한 모듈들이 정확하게 임포트되었습니다.

useState, useGetCrewReviewsQuery, ReviewListSkeleton 모듈이 올바르게 임포트되었습니다.


9-11: 컴포넌트 프로퍼티 인터페이스 정의가 정확합니다.

CrewReviewSectionProps 인터페이스가 crewId를 포함하도록 올바르게 정의되었습니다.


13-13: 컴포넌트에 프로퍼티가 올바르게 전달되었습니다.

CrewReviewSection 컴포넌트가 { crewId }를 받아서 사용하도록 수정되었습니다.


14-15: 상태 관리 및 데이터 페칭이 적절하게 구현되었습니다.

  • useState를 사용하여 페이지 상태를 초기화하고 관리합니다.
  • useGetCrewReviewsQuery를 사용하여 crewIdpage를 기반으로 리뷰 데이터를 가져옵니다.

17-19: 페이지 변경 핸들러가 올바르게 구현되었습니다.

handlePageChange 함수를 통해 페이지 네이션 시 새로운 페이지로 상태를 업데이트합니다.


21-21: 로딩 상태 처리 로직이 적절합니다.

데이터 로딩 중에 ReviewListSkeleton 컴포넌트를 표시하여 사용자 경험을 향상시킵니다.


23-35: 에러 상태 처리

데이터 로드 실패 시 사용자에게 친숙한 에러 메시지를 표시하여 문제를 인지할 수 있게 합니다.


37-44: 리뷰가 없을 때의 처리

리뷰 목록이 비어 있을 경우 사용자에게 안내 메시지를 표시하여 상황을 명확히 전달합니다.


49-49: RatingDisplay 컴포넌트에 필요한 데이터가 정확히 전달되었습니다.

reviewRateInfo 프로퍼티에 데이터를 전달하여 평점 표시가 정확하게 이루어집니다.


51-56: CrewReviewList 컴포넌트에 필수 데이터와 핸들러가 올바르게 전달되었습니다.

  • reviews, totalPages, currentPage 등 필요한 데이터가 정확히 전달됩니다.
  • onPageChange 핸들러를 통해 페이지 변경 시 상태 업데이트가 가능합니다.

Comment on lines +4 to +9
export function useGetCrewReviewsQuery(crewId: number, page: number) {
return useQuery({
queryKey: ['crewReviews', crewId, page],
queryFn: () => getCrewReviews(crewId, page - 1),
});
}
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. page - 1 변환에 대한 주석 설명이 필요합니다
  2. 에러 처리와 재시도 로직이 누락되었습니다
  3. 캐시 전략이 정의되지 않았습니다

다음과 같이 개선해보세요:

 export function useGetCrewReviewsQuery(crewId: number, page: number) {
   return useQuery({
     queryKey: ['crewReviews', crewId, page],
-    queryFn: () => getCrewReviews(crewId, page - 1),
+    queryFn: () => getCrewReviews(crewId, page - 1), // API는 0-based 페이지네이션을 사용
+    staleTime: 5 * 60 * 1000, // 5분
+    retry: 1,
+    onError: (error) => {
+      console.error('크루 리뷰를 불러오는데 실패했습니다:', 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
export function useGetCrewReviewsQuery(crewId: number, page: number) {
return useQuery({
queryKey: ['crewReviews', crewId, page],
queryFn: () => getCrewReviews(crewId, page - 1),
});
}
export function useGetCrewReviewsQuery(crewId: number, page: number) {
return useQuery({
queryKey: ['crewReviews', crewId, page],
queryFn: () => getCrewReviews(crewId, page - 1), // API는 0-based 페이지네이션을 사용
staleTime: 5 * 60 * 1000, // 5분
retry: 1,
onError: (error) => {
console.error('크루 리뷰를 불러오는데 실패했습니다:', error);
},
});
}

Comment on lines +8 to +19
<div className="flex min-w-0 flex-1 flex-col">
<div className="mb-[15px] space-y-1">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-5 w-1/3" />
</div>
<div className="flex items-center gap-2 pb-1">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-5 w-12" />
</div>
<div className="mt-auto">
<Skeleton className="h-4 w-1/4" />
</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 향상을 위한 aria-label 추가가 필요합니다.

스켈레톤 UI의 구조는 잘 잡혀있지만, 스크린 리더 사용자를 위해 로딩 상태임을 알려주는 aria-label을 추가하면 좋을 것 같습니다.

-<div className="flex min-w-0 flex-1 flex-col">
+<div className="flex min-w-0 flex-1 flex-col" aria-label="리뷰 콘텐츠 로딩 중">
   <div className="mb-[15px] space-y-1">
-    <Skeleton className="h-6 w-1/2" />
-    <Skeleton className="h-5 w-1/3" />
+    <Skeleton className="h-6 w-1/2" aria-label="리뷰 제목 로딩 중" />
+    <Skeleton className="h-5 w-1/3" aria-label="리뷰 설명 로딩 중" />
   </div>

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

Comment on lines +41 to +47
export interface ReviewList {
content: ReviewContent[];
pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
}
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

ReviewContent 타입이 정의되어 있지 않은 문제 발견

review.d.ts 파일에서 ReviewList 인터페이스가 ReviewContent[] 타입을 참조하고 있지만, 이 타입이 프로젝트 어디에도 정의되어 있지 않습니다. CrewReview 타입을 사용해야 할 것으로 보입니다.

  • src/types/review.d.ts: ReviewList 인터페이스의 content 속성 타입을 CrewReview[]로 수정이 필요합니다.
🔗 Analysis chain

ReviewContent 타입 정의 확인 필요

ReviewContent 타입이 현재 파일에서 정의되어 있지 않습니다. 이 타입이 다른 파일에 정의되어 있는지 확인이 필요합니다.


Let me search for any type definitions or imports related to ReviewContent.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for ReviewContent type definition

# Search for ReviewContent type definition
ast-grep --pattern 'interface ReviewContent {
  $$$
}'

# Search for ReviewContent type usage
rg "ReviewContent" --type ts

Length of output: 135


Script:

#!/bin/bash
# Check for any imports in review.d.ts
rg "import.*ReviewContent" src/types/review.d.ts

# Check the entire content of review.d.ts
cat src/types/review.d.ts

# Search for type/interface declarations that might contain ReviewContent
ast-grep --pattern 'type ReviewContent = $_'

Length of output: 1274

Comment on lines +34 to +40
id: 1,
gatheringName: '모임모임모임모임모임모밈미밈미미미미미미',
location: '모임모임모임모임모임모밈미밈미미미미미미',
dateTime: '2024-11-21T18:30:00.000Z',
currentCount: 8,
totalCount: 12,
imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.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

테스트 데이터 개선이 필요합니다.

현재 테스트 데이터에 다음과 같은 개선이 필요합니다:

  1. gatheringNamelocation의 길이가 비현실적으로 깁니다. 실제 사용 사례를 반영한 적절한 길이의 데이터로 수정해주세요.
  2. dateTime 형식이 올바른지 확인이 필요합니다.

다음과 같이 수정을 제안드립니다:

-    gatheringName: '모임모임모임모임모임모밈미밈미미미미미미',
-    location: '모임모임모임모임모임모밈미밈미미미미미미',
+    gatheringName: '주말 독서 모임',
+    location: '강남역 카페',
📝 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
id: 1,
gatheringName: '모임모임모임모임모임모밈미밈미미미미미미',
location: '모임모임모임모임모임모밈미밈미미미미미미',
dateTime: '2024-11-21T18:30:00.000Z',
currentCount: 8,
totalCount: 12,
imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.jpg',
id: 1,
gatheringName: '주말 독서 모임',
location: '강남역 카페',
dateTime: '2024-11-21T18:30:00.000Z',
currentCount: 8,
totalCount: 12,
imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.jpg',

Comment on lines +1 to +39
import { Skeleton } from '@mantine/core';

interface ReviewCardSkeletonProps {
isMine?: boolean;
}

export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) {
return (
<div className="w-full">
{isMine && <Skeleton className="mb-3 h-6 w-48" />}
<div
role="presentation"
className={`flex h-full items-end gap-[15px] ${
isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4'
} bg-white lg:gap-[40px]`}
>
<div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]">
{isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />}

<div className="flex-start flex flex-col space-y-4">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>

<div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}>
{!isMine && (
<>
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-24" />
</>
)}
<Skeleton className="h-4 w-12" />
</div>
</div>
</div>
</div>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선이 필요합니다.

스켈레톤 UI에 대한 접근성 고려가 필요합니다. 스크린 리더 사용자를 위한 적절한 aria 속성을 추가하면 좋겠습니다.

다음과 같이 개선해보세요:

   <div className="w-full">
+    <div aria-live="polite" aria-busy="true">
       {isMine && <Skeleton className="mb-3 h-6 w-48" />}
       <div
         role="presentation"
+        aria-label="리뷰 카드 로딩 중"
         className={...}>
         ...
       </div>
+    </div>
   </div>
📝 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
import { Skeleton } from '@mantine/core';
interface ReviewCardSkeletonProps {
isMine?: boolean;
}
export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) {
return (
<div className="w-full">
{isMine && <Skeleton className="mb-3 h-6 w-48" />}
<div
role="presentation"
className={`flex h-full items-end gap-[15px] ${
isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4'
} bg-white lg:gap-[40px]`}
>
<div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]">
{isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />}
<div className="flex-start flex flex-col space-y-4">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}>
{!isMine && (
<>
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-24" />
</>
)}
<Skeleton className="h-4 w-12" />
</div>
</div>
</div>
</div>
);
}
import { Skeleton } from '@mantine/core';
interface ReviewCardSkeletonProps {
isMine?: boolean;
}
export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) {
return (
<div className="w-full">
<div aria-live="polite" aria-busy="true">
{isMine && <Skeleton className="mb-3 h-6 w-48" />}
<div
role="presentation"
aria-label="리뷰 카드 로딩 중"
className={`flex h-full items-end gap-[15px] ${
isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4'
} bg-white lg:gap-[40px]`}
>
<div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]">
{isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />}
<div className="flex-start flex flex-col space-y-4">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}>
{!isMine && (
<>
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-24" />
</>
)}
<Skeleton className="h-4 w-12" />
</div>
</div>
</div>
</div>
</div>
);
}

Comment on lines +41 to +46
classNames={{
control: cn(
'data-[active="true"]:text-blue-500 data-[active="true"]:font-bold',
'border-none bg-transparent hover:bg-transparent',
),
}}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

페이지네이션 컴포넌트의 접근성 개선이 필요합니다.

현재 스타일링은 깔끔하게 구현되었지만, 접근성 측면에서 개선이 필요합니다.

다음과 같은 접근성 개선사항을 추가해보세요:

 classNames={{
   control: cn(
     'data-[active="true"]:text-blue-500 data-[active="true"]:font-bold',
-    'border-none bg-transparent hover:bg-transparent',
+    'border-none bg-transparent hover:bg-transparent focus:ring-2 focus:ring-blue-500',
+    'aria-label="페이지 이동" role="button"',
   ),
 }}

추가로 다음 속성들도 Pagination 컴포넌트에 추가하는 것이 좋습니다:

 <Pagination
   total={totalPages}
   value={currentPage}
   onChange={onPageChange}
+  aria-label="페이지 네비게이션"
+  getItemAriaLabel={(page) => `${page}페이지로 이동`}
   classNames={{

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

Comment on lines +83 to +89
<Button
className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base"
onClick={() => setIsModalOpened(true)}
>
<span className="block md:hidden">리뷰 작성</span>
<span className="hidden md:block">리뷰 작성하기</span>
</Button>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

버튼 접근성 개선이 필요합니다.

버튼 구현이 깔끔하나, 스크린 리더 지원을 위한 추가적인 속성이 필요합니다:

 <Button
   className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base"
   onClick={() => setIsModalOpened(true)}
+  aria-label="모임 리뷰 작성하기"
 >
   <span className="block md:hidden">리뷰 작성</span>
   <span className="hidden md:block">리뷰 작성하기</span>
 </Button>
📝 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
<Button
className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base"
onClick={() => setIsModalOpened(true)}
>
<span className="block md:hidden">리뷰 작성</span>
<span className="hidden md:block">리뷰 작성하기</span>
</Button>
<Button
className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base"
onClick={() => setIsModalOpened(true)}
aria-label="모임 리뷰 작성하기"
>
<span className="block md:hidden">리뷰 작성</span>
<span className="hidden md:block">리뷰 작성하기</span>
</Button>

Comment on lines +57 to +80
<div className="mb-[15px] space-y-1">
<span className="block truncate text-xl font-semibold text-gray-800">
{gatheringName}
</span>
<span className="block truncate text-base font-medium text-gray-700">{location}</span>
</div>
{/* 참여 인원 */}
<div className="flex items-center pb-1">
<div className="relative inline-block h-[20px] w-[20px] flex-shrink-0">
<Image
alt="참여 인원"
src={person}
fill
sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="h-full w-full object-contain"
/>
</div>
<div className="text-sm font-medium text-gray-700">{formatDate}</div>
<span className="mr-2 text-base font-medium text-gray-700">
{currentCount}/{totalCount}
</span>
<Profiles profiles={profiles} />
</div>
<Button
className="bg-blue-500 p-[6px_14px] text-base font-semibold text-white"
onClick={() => setIsModalOpened(true)}
>
리뷰 작성하기
</Button>
{/* 날짜 */}
<div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선을 위한 제안사항입니다.

텍스트 구조화가 잘 되어있으나, 스크린 리더 사용자를 위한 접근성 개선이 필요합니다:

-<span className="block truncate text-xl font-semibold text-gray-800">
+<h2 className="block truncate text-xl font-semibold text-gray-800">
   {gatheringName}
-</span>
+</h2>
-<span className="block truncate text-base font-medium text-gray-700">{location}</span>
+<p className="block truncate text-base font-medium text-gray-700" aria-label="모임 장소">{location}</p>

-<span className="mr-2 text-base font-medium text-gray-700">
+<p className="mr-2 text-base font-medium text-gray-700" aria-label="참여 인원">
   {currentCount}/{totalCount}
-</span>
+</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
<div className="mb-[15px] space-y-1">
<span className="block truncate text-xl font-semibold text-gray-800">
{gatheringName}
</span>
<span className="block truncate text-base font-medium text-gray-700">{location}</span>
</div>
{/* 참여 인원 */}
<div className="flex items-center pb-1">
<div className="relative inline-block h-[20px] w-[20px] flex-shrink-0">
<Image
alt="참여 인원"
src={person}
fill
sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="h-full w-full object-contain"
/>
</div>
<div className="text-sm font-medium text-gray-700">{formatDate}</div>
<span className="mr-2 text-base font-medium text-gray-700">
{currentCount}/{totalCount}
</span>
<Profiles profiles={profiles} />
</div>
<Button
className="bg-blue-500 p-[6px_14px] text-base font-semibold text-white"
onClick={() => setIsModalOpened(true)}
>
리뷰 작성하기
</Button>
{/* 날짜 */}
<div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</div>
<div className="mb-[15px] space-y-1">
<h2 className="block truncate text-xl font-semibold text-gray-800">
{gatheringName}
</h2>
<p className="block truncate text-base font-medium text-gray-700" aria-label="모임 장소">{location}</p>
</div>
{/* 참여 인원 */}
<div className="flex items-center pb-1">
<div className="relative inline-block h-[20px] w-[20px] flex-shrink-0">
<Image
alt="참여 인원"
src={person}
fill
sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="h-full w-full object-contain"
/>
</div>
<p className="mr-2 text-base font-medium text-gray-700" aria-label="참여 인원">
{currentCount}/{totalCount}
</p>
<Profiles profiles={profiles} />
</div>
{/* 날짜 */}
<div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</div>


export interface ReviewRateInfo {
totalReviewCount: number;
totalRate: number;
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

스토리북 파일의 인터페이스가 업데이트되지 않았습니다

rating-display.stories.tsx 파일에서 여전히 totalReviewCount를 사용하고 있으나, 실제 컴포넌트와 타입 정의(review.d.ts)에서는 totalRate로 변경되었습니다. 다음 사항들을 수정해야 합니다:

  • rating-display.stories.tsx 파일의 RatingDisplayStoryProps 인터페이스에서 totalReviewCounttotalRate로 변경
  • argTypes 정의에서 totalReviewCounttotalRate로 변경
  • 모든 스토리 예제(Default, HighRating, LowRating)의 props 이름을 totalRate로 업데이트
🔗 Analysis chain

인터페이스 속성 변경에 따른 영향도 확인 필요

totalReviewCount에서 totalRate로의 속성 이름 변경이 적절해 보입니다. 다만, 이 인터페이스를 사용하는 다른 컴포넌트들에서도 일관되게 변경되었는지 확인이 필요합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 인터페이스 사용 현황 확인

# ReviewRateInfo 인터페이스를 사용하는 모든 파일 검색
rg -l "ReviewRateInfo"

# totalReviewCount나 totalRate 속성을 사용하는 코드 검색
rg "totalReviewCount|totalRate"

Length of output: 1653


Script:

#!/bin/bash
# Let's check the content of review.d.ts to understand the interface definition
cat src/types/review.d.ts

# Let's also check the stories file to understand the discrepancy
cat src/app/\(crew\)/crew/detail/\[id\]/_components/rating-display.stories.tsx

Length of output: 3065

@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (2)
src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx (2)

23-27: 템플릿 함수를 더 간결하게 작성할 수 있습니다.

현재 구현이 명확하지만, 다음과 같이 더 간결하게 작성할 수 있습니다.

-const Template: StoryFn<RatingDisplayStoryProps> = function Template(args) {
-  const { totalRate, averageRate, ratingsData } = args;
-  const reviewRateInfo: ReviewRateInfo = { totalRate, averageRate, ratingsData };
-  return <RatingDisplay reviewRateInfo={reviewRateInfo} />;
+const Template: StoryFn<RatingDisplayStoryProps> = (args) => (
+  <RatingDisplay reviewRateInfo={args} />
+);

Line range hint 31-68: 테스트 케이스 보완이 필요합니다.

현재 기본, 높은 평점, 낮은 평점의 시나리오가 잘 구현되어 있습니다. 하지만 다음과 같은 엣지 케이스를 추가하면 좋을 것 같습니다:

  • 리뷰가 없는 경우 (totalRate: 0)
  • 모든 리뷰가 동일한 점수인 경우
  • 최대 가능한 리뷰 수를 가진 경우

예시 구현:

export const NoReviews = Template.bind({});
NoReviews.args = {
  totalRate: 0,
  averageRate: 0,
  ratingsData: [
    { score: 5, count: 0 },
    { score: 4, count: 0 },
    { score: 3, count: 0 },
    { score: 2, count: 0 },
    { score: 1, count: 0 },
  ],
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0945503 and 6513988.

📒 Files selected for processing (1)
  • src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx (3 hunks)
🔇 Additional comments (2)
src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx (2)

10-13: 속성 이름 변경이 적절히 이루어졌습니다.

totalReviewCount에서 totalRate로의 이름 변경이 일관되게 적용되었으며, 컨트롤 타입과 설명이 적절합니다.


16-20: 인터페이스 정의가 깔끔합니다.

RatingDisplayStoryProps 인터페이스의 타입 정의가 명확하고 일관성 있게 구성되어 있습니다.

Copy link
Contributor

@yulrang yulrang left a comment

Choose a reason for hiding this comment

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

수고많으셨습니다~! 화이팅

@github-actions
Copy link

@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (6)
src/app/(crew)/my-page/_components/profile-card/container.tsx (3)

18-23: 타입 안전성 개선이 필요합니다

user 객체의 타입 안전성을 높이기 위해 옵셔널 체이닝 이외의 추가적인 타입 가드를 고려해보세요.

다음과 같이 타입 가드를 추가하는 것을 제안합니다:

-  const profileImageUrl = user?.profileImageUrl || '';
+  const profileImageUrl = user && 'profileImageUrl' in user ? user.profileImageUrl : '';

Line range hint 26-50: 파일 처리 로직 개선이 필요합니다

파일 크기 제한과 파일 타입을 상수로 분리하고, 파일 타입 검증을 추가하면 좋을 것 같습니다.

다음과 같은 개선을 제안합니다:

+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+const ALLOWED_FILE_TYPES = ['.png', '.jpg', '.jpeg'];
+
 const handleEdit = () => {
   const input = document.createElement('input');
   input.type = 'file';
-  input.accept = '.png,.jpg,.jpeg';
+  input.accept = ALLOWED_FILE_TYPES.join(',');
   input.onchange = async (event) => {
     const file = (event.target as HTMLInputElement)?.files?.[0];
     if (file) {
-      if (file.size > 5 * 1024 * 1024) {
+      if (file.size > MAX_FILE_SIZE) {
         toast.error('5MB 이하의 파일만 업로드 가능합니다.');
         return;
       }
+      if (!ALLOWED_FILE_TYPES.some(type => file.name.toLowerCase().endsWith(type))) {
+        toast.error('지원하지 않는 파일 형식입니다.');
+        return;
+      }

Line range hint 52-61: 삭제 중 로딩 상태 처리가 필요합니다

프로필 이미지 삭제 중에 사용자에게 진행 상태를 표시하면 좋을 것 같습니다.

다음과 같은 개선을 제안합니다:

+  const [isDeleting, setIsDeleting] = useState(false);
+
   const handleDeleteProfile = async () => {
+    if (isDeleting) return;
+    setIsDeleting(true);
     try {
       await resetUserProfileImage();
       await refetchUser();
       toast.success('프로필 이미지가 초기화되었습니다.');
     } catch (error) {
       toast.error('프로필 이미지 초기화에 실패했습니다.');
+    } finally {
+      setIsDeleting(false);
     }
   };
src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx (1)

38-42: 에러 처리 로직 개선 필요

현재 구현은 기본적인 기능은 잘 동작하지만, 다음 사항들을 고려해보시기 바랍니다:

  1. 일반 에러 메시지가 너무 포괄적입니다. 사용자에게 더 구체적인 안내가 필요할 수 있습니다.
  2. window.location.pathname은 SSR 환경에서 문제가 될 수 있습니다.

다음과 같이 개선해보세요:

  if (error instanceof ApiError && error.status === 401) {
-   const redirectUrl = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
+   const redirectUrl = `/login?redirect=${encodeURIComponent(typeof window !== 'undefined' ? window.location.pathname : '')}`;
    router.push(redirectUrl);
  } else {
-   showToast('참여 중 에러가 발생했습니다.', 'error');
+   showToast('모임 참여 처리 중 문제가 발생했습니다. 잠시 후 다시 시도해주세요.', 'error');
  }
src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx (1)

25-30: 에러 처리 로직 개선이 필요합니다

에러 처리가 개선되었지만, 다음과 같은 사항들을 고려해보시면 좋을 것 같습니다:

  • 에러 처리 로직이 handleUnlike와 중복되어 있습니다
  • 401 에러 발생 시 모달 표시 여부를 재검토해볼 필요가 있습니다

다음과 같이 공통 에러 처리 함수를 만들어 사용하는 것을 제안드립니다:

+const handleApiError = (error: unknown, action: string) => {
+  if (error instanceof ApiError && error.status === 401) {
+    setShowLoginModal(true);
+    return;
+  }
+  toast.error(`${action}에 실패했습니다.`);
+};

 const handleLike = async (gatheringId: number) => {
   try {
     await addLike(gatheringId);
     toast.success('찜하기가 완료되었습니다!');
   } catch (error) {
-    if (apiError instanceof ApiError && apiError.status === 401) {
-      toast.error('로그인이 필요합니다.');
-    } else {
-      toast.error('찜하기에 실패했습니다.');
-    }
+    handleApiError(error, '찜하기');
   }
 };
src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx (1)

Line range hint 1-180: 컴포넌트 구조 개선을 위한 제안

전반적으로 잘 구현되어 있으나, 다음과 같은 개선사항을 고려해보시면 좋겠습니다:

  1. 크루 관련 상태 관리 로직(isCaptain, isMember, isJoining, isConfirmed)을 별도의 커스텀 훅으로 분리하여 재사용성과 테스트 용이성을 높일 수 있습니다.
  2. API 에러 처리 로직을 통일된 방식으로 구현하면 좋겠습니다.

다음과 같은 리팩토링을 제안드립니다:

// useCrewState.ts
export function useCrewState(crewData: CrewDetailType, currentUserId?: number) {
  const [isCaptain, setIsCaptain] = useState(false);
  const [isMember, setIsMember] = useState(false);
  const [isConfirmed, setIsConfirmed] = useState(false);

  useEffect(() => {
    if (crewData) {
      setIsConfirmed(crewData.participantCount === crewData.totalCount);

      if (currentUserId) {
        const captain = crewData.crewMembers.find((member) => member.captain);
        const memberExists = crewData.crewMembers.some(
          (member) => member.id === currentUserId
        );

        setIsCaptain(captain?.id === currentUserId);
        setIsMember(memberExists);
      }
    }
  }, [currentUserId, crewData]);

  return { isCaptain, isMember, isConfirmed };
}
// useApiErrorHandler.ts
export function useApiErrorHandler(router: NextRouter) {
  return useCallback((error: unknown, defaultMessage: string) => {
    if (error instanceof ApiError) {
      if (error.status === 401) {
        router.push(
          `/login?redirect=${encodeURIComponent(window.location.pathname)}`
        );
        return;
      }
      toast.error(error.message);
    } else {
      toast.error(defaultMessage);
    }
  }, [router]);
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6513988 and c97c7bf.

📒 Files selected for processing (4)
  • src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx (1 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx (2 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx (1 hunks)
  • src/app/(crew)/my-page/_components/profile-card/container.tsx (2 hunks)
🔇 Additional comments (4)
src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx (2)

4-4: 적절한 라우터 임포트 추가

Next.js의 라우팅 기능을 위한 useRouter 임포트가 올바르게 추가되었습니다.


30-30: 라우터 훅 초기화 확인

컴포넌트 레벨에서 useRouter 훅이 올바르게 초기화되었습니다.

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

38-43: 중복된 에러 처리 로직 발견

handleLike와 동일한 에러 처리 패턴이 반복되고 있습니다. 위에서 제안된 handleApiError 함수를 활용하여 다음과 같이 개선할 수 있습니다:

 const handleUnlike = async (gatheringId: number) => {
   try {
     await removeLike(gatheringId);
     toast.success('찜하기 해제가 완료되었습니다!');
   } catch (error) {
-    if (apiError instanceof ApiError && apiError.status === 401) {
-      toast.error('로그인이 필요합니다.');
-    } else {
-      toast.error('찜하기 해제에 실패했습니다.');
-    }
+    handleApiError(error, '찜하기 해제');
   }
 };

Line range hint 1-99: 전반적인 구현이 잘 되어있습니다! 👍

  • 로딩, 에러, 빈 상태 처리가 잘 구현되어 있습니다
  • TypeScript 타입 정의가 명확합니다
  • 컴포넌트 구조가 깔끔하게 분리되어 있습니다

위에서 제안된 리팩토링 외에는 특별한 문제점이 보이지 않습니다.

Comment on lines 41 to 44
const tempUrl = URL.createObjectURL(file);
setProfileImageUrl(tempUrl);

await refetchUser();
// 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌
toast.success('프로필 이미지가 업데이트되었습니다.');
await refetchUser();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

임시 URL 정리가 필요합니다

URL.createObjectURL로 생성된 임시 URL을 정리하지 않으면 메모리 누수가 발생할 수 있습니다.

다음과 같이 임시 URL을 정리하는 코드를 추가해주세요:

   const tempUrl = URL.createObjectURL(file);
+  // React Query가 refetch를 완료한 후 임시 URL 정리
   await refetchUser();
+  URL.revokeObjectURL(tempUrl);
📝 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 tempUrl = URL.createObjectURL(file);
setProfileImageUrl(tempUrl);
await refetchUser();
// 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌
toast.success('프로필 이미지가 업데이트되었습니다.');
await refetchUser();
const tempUrl = URL.createObjectURL(file);
// 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌
toast.success('프로필 이미지가 업데이트되었습니다.');
await refetchUser();
URL.revokeObjectURL(tempUrl);

Comment on lines +65 to +69
if (joinError.status === 401) {
router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
} else {
toast.error(joinError.message);
}
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

401 에러 처리가 일관되지 않은 부분이 발견되었습니다.

handleLeaveCrewhandleConfirmCancel 함수에서 401 에러에 대한 처리가 누락되어 있습니다. 다음 위치에서 수정이 필요합니다:

  • handleLeaveCrew 함수의 에러 처리 부분에 401 상태 체크 및 로그인 페이지 리다이렉션 추가 필요
  • handleConfirmCancel 함수의 에러 처리 부분에 401 상태 체크 및 로그인 페이지 리다이렉션 추가 필요
🔗 Analysis chain

401 에러 처리 로직이 적절하게 구현되었습니다.

인증되지 않은 사용자의 크루 참여 시도를 적절하게 처리하고 있습니다. 로그인 후 원래 페이지로 돌아올 수 있도록 redirect 파라미터를 포함한 것이 좋은 UX 구현입니다.

다만, 다음 사항을 확인해 주시면 좋겠습니다:

  • 다른 API 호출 함수들(handleLeaveCrew, handleConfirmCancel)에도 동일한 401 에러 처리가 필요한지 검토

아래 스크립트로 다른 API 호출의 401 에러 처리 여부를 확인해보겠습니다:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if other API calls handle 401 errors consistently

# Search for API call implementations
ast-grep --pattern 'catch (${_}Error) {
  if (${_}Error instanceof ApiError) {
    if (${_}Error.status === 401) {
      $$$
    }
  }
}'

Length of output: 139


Script:

#!/bin/bash
# Search for handleLeaveCrew and handleConfirmCancel functions
ast-grep --pattern 'async function handleLeaveCrew() {
  $$$
}'

ast-grep --pattern 'async function handleConfirmCancel() {
  $$$
}'

# Also search for any error handling patterns in these functions
rg "handleLeaveCrew|handleConfirmCancel" -A 10 -B 10

Length of output: 14725

@github-actions
Copy link

@HaeJungg HaeJungg merged commit cc59bce into develop Nov 22, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

크루 리뷰 리스트 추가

4 participants