Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 받은 명함 페이지 - API 연결 로직, 바텀 시트 추가 구현 #149

Merged
merged 22 commits into from
Apr 1, 2025

Conversation

youngju6143
Copy link
Collaborator

@youngju6143 youngju6143 commented Mar 30, 2025

📌 개요

#134 [Feat] 받은 명함 페이지 - API 연결 로직, 바텀 시트 추가 구현

📋 변경사항

받은 명함 페이지에서의 API를 연결하고, 바텀 시트를 추가로 구현했습니다

기능

  • API 연결 :
    • 받은 명함 목록 조회, 사용자 지정 폴더 목록 조회
    • 폴더 생성, 수정, 삭제, 받은 명함 삭제, 받은 명함의 폴더 이동
  • 바텀 시트(받은 명함의 메인 페이지)
    • 폴더 추가, 폴더 수정, 폴더 삭제
  • 바텀 시트, Dialog <명함 선택 페이지>
    • 폴더 설정 -> 사용자가 지정한 폴더만 뜨게 수정
    • 삭제 -> 삭제 Dialog 띄움

화면

기능 스크린샷
받은 명함 메인 페이지 Screenshot 2025-03-30 at 11 01 38 PM
받은 명함 선택 페이지 Screenshot 2025-03-30 at 11 02 19 PM
바텀 시트 Screenshot 2025-03-30 at 11 02 59 PM

✅ 체크사항

  • 기능이 정상적으로 동작하는지 확인
  • 코드 스타일 및 규칙 준수 확인
  • UI가 변경된 경우 스크린샷 첨부 여부 확인

기타

  • 수정하고 추가한 코드가 너무 많아서 리뷰가 필요한 부분만 코멘트 달아놓겠습니다(API 로직)
  • QA에서 요구한 수정사항은 추후 추가 예정입니다
  • 데이터를 불러올 때 임시로 로딩 컴포넌트를 구현해놓았으며, 이는 디자인이 나오면 추후 수정 예정입니다

Summary by CodeRabbit

  • 새로운 기능
    • 수신명함 관리 페이지에 새로운 컴포넌트가 추가되어 카드 조회 및 관리가 개선되었습니다.
    • 폴더 관리 기능이 강화되어, 폴더 추가, 수정, 삭제가 가능해졌습니다.
    • 여러 카드 선택 후 삭제 및 이동 기능이 도입되어 카드 관리가 더욱 효율적입니다.
    • 사용자 인터페이스에 폴더 목록과 선택 기능이 추가되어 폴더 관리가 직관적으로 이루어집니다.
    • 토스트 알림을 통해 사용자에게 피드백을 제공하는 기능이 추가되었습니다.

@youngju6143 youngju6143 added the 🚀 feature 새로운 기능 개발 label Mar 30, 2025
@youngju6143 youngju6143 self-assigned this Mar 30, 2025
@youngju6143 youngju6143 linked an issue Mar 30, 2025 that may be closed by this pull request
13 tasks
Copy link

coderabbitai bot commented Mar 30, 2025

📝 Walkthrough

Walkthrough

이 PR은 받은 카드와 폴더 관리를 위한 UI, 상태 관리, API 호출 로직을 새로 추가하거나 수정합니다. 주요 변경 사항으로는 받은 카드 데이터를 여러 개로 처리하도록 쿼리 훅 및 컴포넌트 개선, 폴더 데이터를 위한 타입 및 상태 관리(Zustand 스토어) 도입, 모달 및 토스트 컴포넌트 인터페이스 개편, 그리고 폴더와 카드에 대한 생성, 수정, 삭제, 이동을 위한 mutation 훅 추가가 있습니다.

Changes

File(s) Change Summary
src/app/(received)/received/choose/page.tsx, src/app/(received)/received/interesting/page.tsx, src/app/(received)/received/page.tsx 받은 카드 관련 Page 컴포넌트 추가 및 수정. useReceivedCardsQuery, useFoldersQuery 도입 및 상태 관리, 폴더 선택 기능 추가.
src/entities/folder/dto.ts, src/entities/folder/types.ts 폴더 데이터 구조를 위한 FolderDtoFolder 타입 추가.
src/features/received/config/index.ts 폴더 샘플 데이터를 포함하는 FOLDERS_MOCK 상수 추가.
src/features/received/model/queries/useFoldersQuery.ts, src/features/received/model/queries/useReceivedCardsQuery.ts, (삭제됨: src/features/received/model/queries/useReceivedCardQuery.ts) 쿼리 훅 네이밍 변경 및 리팩토링: useFoldersQuery 개선, useReceivedCardsQuery 신규 추가, 기존 단일 카드 훅 제거.
src/features/received/model/store/useFoldersStore.ts, src/features/received/model/store/useReceivedCardsStore.ts Zustand 기반 폴더 및 받은 카드 관리 스토어 추가.
src/features/received/model/useDialog.ts 대화상자 상태 관리를 위한 useDialog 커스텀 훅 추가.
src/features/received/ui/choose/chooseReceivedCardView.tsx, (삭제됨: src/features/received/ui/chooseReceivedCardView.tsx) 받은 카드 선택 UI 컴포넌트 갱신: 새로운 위치에 구현하고 기존 파일 삭제.
src/features/received/ui/foldersList.tsx 폴더 리스트 렌더링 및 선택 기능을 제공하는 FoldersList 컴포넌트 추가.
src/features/received/ui/receivedCard.tsx ReceivedCard 컴포넌트의 이미지 alt 속성 수정.
src/features/received/ui/receivedCardList.tsx 선택된 폴더에 따른 받은 카드 리스트를 렌더링하는 ReceivedCardList 컴포넌트 추가.
src/features/received/ui/receivedCardView.tsx 폴더 및 카드 관리를 위한 ReceivedCardView 컴포넌트 수정: props 추가 및 모달 개선.
src/shared/ui/Toast/index.tsx, src/shared/ui/Toast/sonner.tsx Toast 컴포넌트 인터페이스 수정(불필요한 props 삭제) 및 스타일 클래스 업데이트.
src/features/received/model/mutations/(useCreateFolder.ts, useDeleteFolder.ts, useDeleteReceivedCard.ts, useEditFolder.ts, useMoveCardToFolder.ts) 폴더 생성, 수정, 삭제 및 카드 이동/삭제를 위한 mutation 훅 추가.

Possibly related PRs

Suggested reviewers

  • jangwonyoon
  • semnil5202
  • evenway2025
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @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.

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.

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: 5

🧹 Nitpick comments (34)
src/features/received/model/useDialog.ts (1)

1-18: 다이얼로그 상태 관리를 위한 훅이 깔끔하게 구현되었습니다.

useState를 사용한 간결한 구현과 명확한 함수명(openDialog, closeDialog)으로 가독성이 좋습니다. 하나의 책임만 가지고 있어 재사용성이 높은 훅입니다.

다만, 여러 다이얼로그를 관리해야 하는 경우를 대비해 식별자(id)를 옵션으로 받도록 확장하는 것도 고려해 보세요.

- export function useDialog() {
-   const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
+ export function useDialog(id?: string) {
+   const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
src/app/(received)/received/choose/page.tsx (1)

13-34: 로딩 및 오류 상태 처리 추가 필요

현재 컴포넌트는 데이터 로딩 중이거나 오류가 발생했을 때의 UI 처리가 없습니다. 사용자 경험을 개선하기 위해 로딩 상태와 오류 상태에 대한 UI를 추가하는 것이 좋습니다.

function Page() {
  const { cards: serverReceivedCards, isLoading: isCardsLoading } = useReceivedCardsQuery(null);
  const handleBack = useHistoryBack();

  const { setReceivedCards } = useReceivedCardsStore();
  useEffect(() => {
    if (!isCardsLoading) setReceivedCards(serverReceivedCards);
  }, [isCardsLoading, serverReceivedCards, setReceivedCards]);

+  if (isCardsLoading) {
+    return (
+      <div className="flex h-dvh w-full justify-center">
+        <div className="flex w-full max-w-[600px] flex-col bg-gray-black">
+          <Appbar page="create" onLeftClick={handleBack} />
+          <div className="flex h-full items-center justify-center">
+            <p className="text-white">로딩 중...</p>
+          </div>
+        </div>
+      </div>
+    );
+  }

  return (
    <div className="flex h-dvh w-full justify-center">
      <div className="flex w-full max-w-[600px] flex-col bg-gray-black">
        <Appbar page="create" onLeftClick={handleBack} />
        <div className="overflow-y-auto px-5 pb-24 scrollbar-hide">
          <ChooseReceivedCardView />
        </div>
        <Navbar />
        <Toast />
      </div>
    </div>
  );
}
src/features/received/ui/receivedCardList.tsx (2)

16-16: 에러 및 빈 상태 처리가 필요합니다.

현재 로딩 상태는 처리되고 있지만, 에러 발생 시와 데이터가 없을 때의 처리가 빠져 있습니다. 사용자 경험을 향상시키기 위해 이러한 상태들도 처리해 주세요.

- if (isLoading || isFetching) return <p>받은 명함들 로딩중이에요...</p>; // 임시 로딩 구현
+ if (isLoading || isFetching) return <p>받은 명함들 로딩중이에요...</p>; // 임시 로딩 구현
+ 
+ // 에러 상태 처리
+ // const { isError } = useReceivedCardsQuery(selectedFolderId);
+ // if (isError) return <p>명함을 불러오는 중 오류가 발생했습니다.</p>;
+ 
+ // 빈 상태 처리
+ if (receivedCards.length === 0) return <p>받은 명함이 없습니다.</p>;

13-14: 쿼리 훅 사용 개선하기

useReceivedCardsQuery 훅에서 반환되는 refetch 함수를 활용하여 데이터를 다시 불러오는 기능을 추가할 수 있습니다. 또한 선택한 폴더 ID가 변경될 때 데이터가 올바르게 갱신되는지 확인해 보세요.

src/features/received/model/store/useReceivedCardsStore.ts (3)

3-3: feature 간 결합도 확인

Card 타입을 home 기능에서 가져오는 것은 기능 간의 강한 결합을 만들 수 있습니다. 여러 기능에서 공유되는 타입이라면 shared 디렉토리로 이동하는 것을 고려해 보세요.

- import { Card } from '@/features/home/types';
+ import { Card } from '@/shared/types';

13-17: 데이터 유효성 검사 추가

스토어 메서드에 입력 유효성 검사를 추가하여 런타임 오류를 방지하는 것이 좋습니다. 또한 카드 상태 변경 시 디버깅을 위한 로깅을 고려해 보세요.

  setReceivedCards: (receivedCards) => set({ receivedCards }),
  deleteCards: (cardIds) =>
    set((state) => ({
-     receivedCards: state.receivedCards.filter((receivedCard) => !cardIds.includes(receivedCard.id)),
+     receivedCards: state.receivedCards.filter((receivedCard) => {
+       // id가 undefined인 경우 방어 로직
+       if (receivedCard.id === undefined) return true;
+       return !cardIds.includes(receivedCard.id);
+     }),
    })),

5-9: 추가 기능 고려

현재 스토어는 카드 목록 설정과 삭제만 지원합니다. 애플리케이션 요구 사항에 따라 단일 카드 추가, 카드 업데이트, 폴더 간 카드 이동 등의 기능을 추가하는 것을 고려해 보세요.

src/shared/ui/Toast/sonner.tsx (1)

18-18: 스타일 클래스 가독성 개선

긴 클래스 문자열은 읽기 어렵고 유지 관리하기 어렵습니다. Tailwind CSS의 @apply 지시문을 사용하거나 별도의 클래스로 분리하는 것을 고려해 보세요. 또한 하드코딩된 마진 값(mb-20)은 다양한 화면 크기에 적합하지 않을 수 있으므로 반응형 값으로 대체하는 것이 좋습니다.

- 'group toast flex justify-center items-center max-w-max group-[.toaster]:bg-gray-600 group-[.toaster]:text-gray-white group-[.toaster]:text-body-4 group-[.toaster]:rounded-full group-[.toaster]:border-none group-[.toaster]:m-auto group-[.toaster]:mb-20',
+ [
+   'group toast flex justify-center items-center max-w-max',
+   'group-[.toaster]:bg-gray-600 group-[.toaster]:text-gray-white group-[.toaster]:text-body-4',
+   'group-[.toaster]:rounded-full group-[.toaster]:border-none',
+   'group-[.toaster]:m-auto group-[.toaster]:mb-20 md:group-[.toaster]:mb-16 sm:group-[.toaster]:mb-10',
+ ].join(' '),
src/shared/ui/Toast/index.tsx (2)

8-13: 사용 방법 문서 개선

현재 문서에서는 toast.success(message) 또는 toast.error(message)를 사용하라고 안내하고 있지만, toast 객체가 어디서 오는지 명확히 설명되어 있지 않습니다. 문서에 import 예시를 추가하여 명확히 해주세요.

/**
 * 공통 컴포넌트 - sonner(toast)
 *
 * 사용 방법 :
 * @example
+ * import { toast } from 'sonner';
 * 토스트 띄울 함수에 toast.success(message) || toast.error(message)
 * <Toast />
 *

24-30: alt 텍스트 수정 필요

성공 아이콘의 alt 텍스트가 "error-icon"으로 설정되어 있어 스크린 리더 사용자에게 혼란을 줄 수 있습니다. 적절한 alt 텍스트로 수정해주세요.

<Image
  src="/icons/toast-icon/checkIcon.svg"
  className="text-secondary"
- alt="error-icon"
+ alt="success-icon"
  width={24}
  height={24}
/>
src/features/received/model/queries/useReceivedCardsQuery.ts (1)

1-33: API 연동 로직이 잘 구현되었습니다.

폴더 기반으로 카드를 조회하는 기능이 적절하게 구현되었습니다. 몇 가지 개선할 사항이 있습니다:

  1. 쿼리 키가 고정되어 있어 folderId가 변경될 때 자동으로 새로운 쿼리가 실행되지 않을 수 있습니다.
  2. 콘솔 로그가 프로덕션 코드에 남아있습니다.

다음과 같이 개선하시는 것을 권장합니다:

export const useReceivedCardsQuery = (folderId: number | null) => {
  const { data, isLoading, isFetching, isError, refetch } = useQuery({
-    queryKey: ['cards'],
+    queryKey: ['cards', folderId],
    queryFn: () => _getReceivedCards(folderId),
    enabled: true,
  });

  return { cards: data?.cards ?? [], isLoading, isFetching, isError, refetch };
};

또한 프로덕션에 배포하기 전에 콘솔 로그(16번 줄)를 제거하는 것을 고려해보세요.

src/features/received/ui/foldersList.tsx (1)

1-42: 폴더 리스트 구현이 깔끔합니다.

폴더 선택 기능이 잘 설계되어 있습니다. 다음의 개선 사항을 고려해보세요:

  1. 배열 인덱스를 키로 사용하는 것은 React에서 권장되지 않습니다.
  2. 로딩 메시지가 하드코딩되어 있습니다.
{folders.map((folder, index) => {
  return (
    <Tag
-     key={index}
+     key={folder.id}
      size="lg"
      message={folder.name}
      className={tagStyle}
      onClick={() => handleFolderSelect(folder.id)}
    />
  );
})}

또한 로딩 상태 처리를 위한 별도의 로딩 컴포넌트를 사용하거나, 다국어 지원을 위한 시스템을 적용하는 것을 고려해보세요.

src/features/received/model/queries/useDeleteFolder.ts (1)

16-27: 폴더 삭제 뮤테이션 훅이 잘 구현되었습니다.

폴더 삭제 기능이 적절하게 구현되었습니다. 다음의 개선 사항을 고려해보세요:

  1. 주석 처리된 코드가 있습니다.
  2. 성공/실패 콜백에서 쿼리 무효화나 추가 액션이 없습니다.
export const useDeleteFolder = () => {
  return useMutation({
-    // mutationFn: (folderId: number) => _deleteFolder(folderId),
    mutationFn: (variables: { folderId: number }) => _deleteFolder(variables.folderId),
    onSuccess: () => {
      console.log('폴더 삭제 성공');
+     // queryClient.invalidateQueries({ queryKey: ['folders'] });
    },
    onError: (error) => {
      console.error('폴더 삭제 실패:', error);
    },
  });
};

뮤테이션 성공 후 폴더 목록을 갱신하기 위해 queryClient를 사용하여 관련 쿼리를 무효화하는 것을 고려해보세요. 이를 위해 useQueryClient를 import하고 queryClient를 선언해야 합니다.

src/features/received/model/queries/useMoveCardToFolder.ts (1)

19-30: 카드 이동 뮤테이션 훅이 잘 구현되었습니다.

카드를 폴더로 이동하는 기능이 적절하게 구현되었습니다. 다음의 개선 사항을 고려해보세요:

  1. 성공/실패 콜백에서 쿼리 무효화나 상태 업데이트가 없습니다.
  2. 프로덕션 코드에 콘솔 로그가 남아있습니다.
export const useMoveCardToFolder = () => {
+  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (variables: { folderId: number; cardIds: number[] }) =>
      _moveCardToFolder(variables.folderId, variables.cardIds),
    onSuccess: () => {
      console.log('받은 명함을 폴더에 저장 성공');
+     queryClient.invalidateQueries({ queryKey: ['cards'] });
    },
    onError: (error) => {
      console.error('받은 명함을 폴더에 저장 실패 :', error);
    },
  });
};

뮤테이션 성공 후 데이터 상태를 갱신하기 위해 queryClient를 사용하여 관련 쿼리를 무효화하는 것을 고려해보세요. 또한, 프로덕션 환경에서는 콘솔 로그를 제거하거나 로깅 시스템을 사용하는 것이 좋습니다.

src/features/received/model/store/useFoldersStore.ts (3)

13-37: 폴더 ID 생성 로직 개선 필요

addFolder 함수에서 새 폴더의 ID를 계산하는 방식이 잠재적으로 문제를 일으킬 수 있습니다. 현재는 마지막 폴더의 ID + 1로 계산하고 있는데, 폴더가 삭제된 후 새 폴더가 추가될 경우 ID가 중복될 가능성이 있습니다.

  addFolder: (folderName) =>
    set((state) => {
-      const lastId = state.folders.length > 0 ? state.folders[state.folders.length - 1].id : 0;
+      // 기존 ID 중 최대값을 찾아 + 1 하거나, 첫 폴더인 경우 1 사용
+      const maxId = state.folders.length > 0 ? Math.max(...state.folders.map(folder => folder.id)) : 0;
      return {
-        folders: [...state.folders, { id: lastId + 1, name: folderName }],
+        folders: [...state.folders, { id: maxId + 1, name: folderName }],
      };
    }),

8-9: 빈 폴더명 검증 로직 추가 필요

현재 폴더 이름에 대한 유효성 검사가 없어 빈 문자열 또는 공백만 있는 이름의 폴더가 생성될 수 있습니다.

-  addFolder: (folderName: string) => void;
+  addFolder: (folderName: string) => { success: boolean; message?: string };

그리고 구현부 수정:

  addFolder: (folderName) =>
    set((state) => {
+      // 폴더명 유효성 검사
+      if (!folderName.trim()) {
+        return { ...state, success: false, message: '폴더명을 입력해주세요.' };
+      }
      const lastId = state.folders.length > 0 ? state.folders[state.folders.length - 1].id : 0;
      return {
        folders: [...state.folders, { id: lastId + 1, name: folderName }],
+        success: true,
      };
    }),

23-32: 폴더 업데이트 성공/실패 상태 반환 필요

updateFolder 함수에서 해당 ID의 폴더를 찾지 못했을 때 상태 변경 없이 그대로 반환하고 있어, 호출자가 업데이트가 성공했는지 실패했는지 알 수 없습니다.

-  updateFolder: (id: number, newFolderName: string) => void;
+  updateFolder: (id: number, newFolderName: string) => { success: boolean; message?: string };

  updateFolder: (id, newFolderName) =>
    set((state) => {
      const index = state.folders.findIndex((folder) => folder.id === id);
      if (index !== -1) {
        const updatedFolders = [...state.folders];
        updatedFolders[index] = { ...updatedFolders[index], name: newFolderName };
-        return { folders: updatedFolders };
+        return { folders: updatedFolders, success: true };
      }
-      return state;
+      return { ...state, success: false, message: '폴더를 찾을 수 없습니다.' };
    }),
src/features/received/model/queries/useDeleteReceivedCard.ts (2)

6-14: 에러 처리 개선 필요

_deleteReceivedCards 함수 내에서 에러를 캐치하고 다시 던지는 부분이 있는데, 이후 useMutation에서도 같은 에러를 처리하고 있어 중복됩니다. 한 곳에서만 에러를 처리하는 것이 좋습니다.

 const _deleteReceivedCards = async (cardIds: number[]) => {
-  try {
-    const response = await client.delete(`${CLIENT_SIDE_URL}/api/card/receive`, { data: { cardIds } });
-    return response;
-  } catch (error) {
-    console.error('받은 명함 삭제 실패 :', error);
-    throw error;
-  }
+  return await client.delete(`${CLIENT_SIDE_URL}/api/card/receive`, { data: { cardIds } });
 };

19-28: 사용자 피드백 제공 방식 개선 필요

현재 구현에서는 성공이나 실패 시 콘솔에만 로그를 출력하고 있습니다. 사용자에게 적절한 피드백을 제공하도록 개선하면 좋겠습니다.

 return useMutation({
   mutationFn: (variables: { cardIds: number[] }) => _deleteReceivedCards(variables.cardIds),
   onSuccess: () => {
     queryClient.invalidateQueries({ queryKey: ['cards'] });
-    console.log('받은 명함 삭제 성공');
+    // 이미 호출 컴포넌트에서 toast 등을 통해 사용자에게 피드백을 제공하고 있다면 로그는 제거
   },
   onError: (error) => {
-    console.error('받은 명함 삭제 실패:', error);
+    // 에러 발생 시 콜백 함수를 props로 받아 호출하거나 상태를 통해 컴포넌트에 알림
+    return error; // 호출 컴포넌트에서 에러 처리 가능하도록 반환
   },
 });
src/features/received/model/queries/useFoldersQuery.ts (3)

11-19: 불필요한 로그 및 주석 정리 필요

함수 내에 "추후 지우기"라는 주석과 함께 콘솔 로그가 있습니다. 프로덕션 코드에는 디버깅용 로그를 남기지 않는 것이 좋습니다.

 const _getFolders = async () => {
   try {
     const { data } = await client.get<FolderDto>(`${CLIENT_SIDE_URL}/api/card/folders`);
-    console.log('서버로부터 받은 데이터 : ', data); // 추후 지우기
     return data;
   } catch (error) {
     console.error(error);
     throw error;
   }
 };

22-31: 미사용 코드 정리 및 주석 개선 필요

현재 코드에는 "추후 수정", "추후 API 호출을 통해 받은 데이터로 수정" 등의 주석이 있고, 주석 처리된 코드도 있습니다. 이는 코드 가독성을 해치므로 정리가 필요합니다.

 export const useFoldersQuery = () => {
   const { data, isLoading, isError } = useQuery({
-    // const { data: _ } = useQuery({
-    queryKey: ['folders'], // 추후 수정
+    queryKey: ['folders'],
     queryFn: _getFolders,
   });
-  return { folders: data?.folders ?? [], isLoading, isError }; // 추후 API 호출을 통해 받은 데이터로 수정
+  return { folders: data?.folders ?? [], isLoading, isError };

-  // return { data: FOLDERS_MOCK };
 };

9-9: 미사용 상수 정리 필요

FOLDER_QUERY_KEY 상수가 정의되어 있지만 실제로 사용되지 않고 있습니다. 쿼리 키로 ['folders']를 사용하고 있으므로, 이 상수는 제거하거나 실제 사용하는 값으로 변경해야 합니다.

-export const FOLDER_QUERY_KEY = 'FOLDER_QUERY_KEY';
+// 상수를 실제로 사용하지 않으므로 제거하거나 아래와 같이 변경
+export const FOLDER_QUERY_KEY = 'folders';

그리고 사용 부분 수정:

   const { data, isLoading, isError } = useQuery({
-    queryKey: ['folders'],
+    queryKey: [FOLDER_QUERY_KEY],
     queryFn: _getFolders,
   });
src/features/received/ui/choose/chooseReceivedCardView.tsx (5)

31-34: 불필요한 콘솔 로그 제거 필요

toggleChecked 함수에 불필요한 디버깅용 콘솔 로그가 남아있습니다. 프로덕션 코드에서는 이러한 로그를 제거하는 것이 좋습니다.

 const toggleChecked = (id: number) => {
   setSelectedCardIds((prev) => (prev.includes(id) ? prev.filter((cardId) => cardId !== id) : [...prev, id]));
-  console.log(selectedCardIds);
 };

49-54: 불필요한 콘솔 로그 및 의존성 배열 최적화 필요

useEffect 훅 내에 "asdfasdfa"라는 불필요한 콘솔 로그가 있습니다. 또한, isFetching을 의존성으로 사용하는 것은 매 요청마다 상태를 초기화할 수 있어 불필요한 리렌더링을 유발할 수 있습니다.

 useEffect(() => {
-  if (!isFetching) {
+  // 초기 로딩이 완료되었을 때만 상태 업데이트
+  if (!isLoading && cards.length > 0) {
     setReceivedCards(cards);
   }
-  console.log('asdfasdfa');
-}, [isFetching]);
+}, [isLoading, cards]);

56-56: 로딩 UI 개선 필요

현재 로딩 상태는 간단한 텍스트만 표시하고 있습니다. 더 나은 사용자 경험을 위해 로딩 스피너나 스켈레톤 UI를 사용하는 것이 좋습니다.

-  if (isFetching) return <p>받은 명함들 로딩중이에요...</p>; // 임시 로딩 구현
+  if (isFetching) {
+    return (
+      <div className="flex justify-center items-center h-full w-full py-8">
+        <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900"></div>
+        <p className="ml-2 text-gray-600">받은 명함들 로딩중이에요...</p>
+      </div>
+    );
+  }

85-98: 카드 목록 최적화 필요

현재 카드 목록에 대한 최적화가 없어 항목이 많아질 경우 성능 문제가 발생할 수 있습니다. 또한 카드가 없을 때의 처리도 필요합니다.

+      {receivedCards.length === 0 && (
+        <div className="flex flex-col items-center justify-center py-8">
+          <p className="text-gray-500">받은 명함이 없습니다.</p>
+        </div>
+      )}
       {receivedCards.map((card, index) => {
         return (
-          <div key={index} className="flex max-w-full items-center gap-4">
+          <div key={card.id} className="flex max-w-full items-center gap-4">
             <ReceivedCheckbox
               checked={selectedCardIds.includes(card.id)}
               onCheckedChange={() => toggleChecked(card.id)}
             />

             <div className="min-w-0 flex-1">
-              <ReceivedCard key={index} cardData={card} />
+              <ReceivedCard cardData={card} />
             </div>
           </div>
         );
       })}

28-28: isFetching만 사용하는 문제

useReceivedCardsQuery에서 isLoading 대신 isFetching만 사용하고 있습니다. isLoading은 초기 로딩 상태를, isFetching은 모든 데이터 페칭 작업에 대해 true가 되므로, 상황에 맞게 적절히 사용해야 합니다.

-  const { cards, isFetching } = useReceivedCardsQuery(null);
+  const { cards, isLoading, isFetching } = useReceivedCardsQuery(null);

그리고 로딩 표시 부분 수정:

-  if (isFetching) return <p>받은 명함들 로딩중이에요...</p>; // 임시 로딩 구현
+  if (isLoading) return <p>받은 명함들 로딩중이에요...</p>; // 초기 로딩만 체크
src/app/(received)/received/interesting/page.tsx (1)

27-27: 반복문에서 key로 index 사용을 지양해주세요.
serverReceivedCards.map((value, index) => ...) 사용 시, 데이터 순서가 변경되면 예기치 않은 이슈가 생길 수 있습니다. 가능하다면 card.id 같은 고유값을 key로 사용하는 것이 좋습니다.

src/features/received/ui/receivedCardView.tsx (4)

26-32: 상태 분리가 잘 되어 있지만, boolean 플래그가 늘어날 경우 구조 개선을 고려해주세요.
isUpdateisAdd 같이 두 개 이상의 상태를 boolean으로 관리할 때 상태가 많이 증가하면 로직이 복잡해질 수 있습니다. 필요한 경우 enum 등을 활용하여 하나의 상태로 관리하는 방안도 고려 가능합니다.


72-82: 새 폴더 추가 시 사용자 입력값 검증이 필요할 수 있습니다.
현재 빈 문자열 입력을 막는 로직이 없으므로, 폴더 이름이 비어있는 상태로 추가되는 상황을 확인해 주세요. 유효성 검사를 추가해 주시면 좋겠습니다.


96-99: 디버그용 console.log 제거 고려
프로덕션 환경에서 사용하지 않는 console.log는 추후 유지보수 시 제거하는 것이 좋습니다.


118-160: Modal 내용을 동적으로 구성하는 방식이 직관적입니다.

  • folders.map((folder, index) 부분에서 index 대신 folder.id를 key에 사용하면 좋습니다.
  • 모달에서 폴더 이름 길이 제한(예: "10/10") 표시는 있지만, 실제 유효성 로직이 없어 사용자가 의도치 않게 제한을 넘을 수 있으니 확인 필요합니다.
src/app/(received)/received/page.tsx (2)

29-31: 폴더 데이터 로딩 완료 후 스토어 업데이트
isFoldersLoading가 false일 때 분기 처리를 하는 로직이 좋습니다. 다만, 폴더 데이터가 변경될 때도 감지하려면 의존성 배열에 serverFolders를 넣을지 고려해 보세요.


33-35: 카드 데이터 로딩 완료 후 스토어 업데이트
폴더 로직과 동일하게 isCardsLoading 대신 카드 데이터 변화 여부를 감지하는 방법도 고려해 보세요.

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6318b8a and 090ff2a.

📒 Files selected for processing (25)
  • src/app/(received)/received/choose/page.tsx (1 hunks)
  • src/app/(received)/received/interesting/page.tsx (2 hunks)
  • src/app/(received)/received/page.tsx (1 hunks)
  • src/entities/folder/dto.ts (1 hunks)
  • src/entities/folder/types.ts (1 hunks)
  • src/features/received/config/index.ts (1 hunks)
  • src/features/received/model/queries/useCreateFolder.ts (1 hunks)
  • src/features/received/model/queries/useDeleteFolder.ts (1 hunks)
  • src/features/received/model/queries/useDeleteReceivedCard.ts (1 hunks)
  • src/features/received/model/queries/useEditFolder.ts (1 hunks)
  • src/features/received/model/queries/useFoldersQuery.ts (1 hunks)
  • src/features/received/model/queries/useMoveCardToFolder.ts (1 hunks)
  • src/features/received/model/queries/useReceivedCardQuery.ts (0 hunks)
  • src/features/received/model/queries/useReceivedCardsQuery.ts (1 hunks)
  • src/features/received/model/store/useFoldersStore.ts (1 hunks)
  • src/features/received/model/store/useReceivedCardsStore.ts (1 hunks)
  • src/features/received/model/useDialog.ts (1 hunks)
  • src/features/received/ui/choose/chooseReceivedCardView.tsx (1 hunks)
  • src/features/received/ui/chooseReceivedCardView.tsx (0 hunks)
  • src/features/received/ui/foldersList.tsx (1 hunks)
  • src/features/received/ui/receivedCard.tsx (1 hunks)
  • src/features/received/ui/receivedCardList.tsx (1 hunks)
  • src/features/received/ui/receivedCardView.tsx (3 hunks)
  • src/shared/ui/Toast/index.tsx (1 hunks)
  • src/shared/ui/Toast/sonner.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • src/features/received/model/queries/useReceivedCardQuery.ts
  • src/features/received/ui/chooseReceivedCardView.tsx
🧰 Additional context used
🧬 Code Definitions (7)
src/entities/folder/dto.ts (1)
src/entities/folder/types.ts (1)
  • Folder (1-4)
src/app/(received)/received/choose/page.tsx (2)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (25-33)
src/features/received/ui/choose/chooseReceivedCardView.tsx (1)
  • ChooseReceivedCardView (20-112)
src/features/received/ui/receivedCardList.tsx (1)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (25-33)
src/features/received/model/store/useFoldersStore.ts (1)
src/entities/folder/types.ts (1)
  • Folder (1-4)
src/features/received/ui/choose/chooseReceivedCardView.tsx (4)
src/features/received/model/store/useReceivedCardsStore.ts (1)
  • useReceivedCardsStore (11-18)
src/features/received/model/queries/useDeleteReceivedCard.ts (1)
  • useDeleteReceivedCards (16-29)
src/features/received/model/queries/useMoveCardToFolder.ts (1)
  • useMoveCardToFolder (19-30)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (25-33)
src/features/received/ui/foldersList.tsx (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • useFoldersQuery (22-31)
src/app/(received)/received/interesting/page.tsx (1)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (25-33)
🔇 Additional comments (30)
src/entities/folder/types.ts (1)

1-4: 간결하고 명확한 타입 정의를 잘 구현하셨습니다.

폴더 타입이 필요한 속성만 포함하고 있어 가독성이 좋습니다. ID와 이름은 폴더 관리에 필수적인 속성이며 타입도 적절하게 지정되어 있습니다.

src/entities/folder/dto.ts (1)

1-10: API 응답 구조가 잘 정의되어 있습니다.

API 응답에 필요한 모든 표준 필드(status, message, timestamp)와 데이터 구조가 명확히 정의되어 있습니다. Folder 타입을 재사용하여 일관성을 유지한 점도 좋습니다.

src/features/received/config/index.ts (1)

77-94:

❓ Verification inconclusive

목업 데이터가 잘 구성되어 있으나 프로덕션 코드에서 사용 여부를 확인해주세요.

목업 데이터는 테스트나 개발 환경에서만 사용되어야 합니다. 프로덕션 환경에서는 실제 API 응답을 사용하는지 확인해주세요. 또한 나중에 목업 데이터를 쉽게 찾아 제거할 수 있도록 주석을 추가하는 것이 좋을 것 같습니다.


🏁 Script executed:

#!/bin/bash
# Check if the mock data is being used in production code

echo "Checking for references to FOLDERS_MOCK in production code..."
rg -l "FOLDERS_MOCK" --type ts --type tsx | grep -v "config/index.ts" | grep -v "test" | grep -v "mock"

Length of output: 258


프로덕션 코드에서 목업 데이터 사용 여부 재확인 필요

현재 코드에서는 목업 데이터(FOLDERS_MOCK)가 잘 구성되어 있습니다. 다만, 해당 데이터가 프로덕션 환경에서 사용되고 있지 않은지 추가 검증이 필요합니다. 아래 사항들을 점검해주세요:

  • 프로덕션 코드 참조 확인:
    목업 데이터가 테스트나 개발 환경에서만 사용되어야 하므로, 실제 프로덕션 코드(예: src/features/received/config/index.ts 외 다른 파일)에서 FOLDERS_MOCK을 참조하고 있는지 확인해 주세요.

  • 프로덕션 API 응답 사용 여부:
    만약 프로덕션 코드에서 해당 목업 데이터를 사용 중이라면, 실제 API 응답으로 대체되어야 합니다.

  • 향후 관리 용이성:
    목업 데이터임을 명시하는 주석을 추가하여 나중에 쉽게 찾아 제거할 수 있도록 해주세요.

검증을 돕기 위해 아래와 같이 수정된 스크립트를 실행해 볼 것을 제안합니다:

#!/bin/bash
echo "Searching for references to FOLDERS_MOCK in production code..."
rg -l "FOLDERS_MOCK" -g "*.ts" -g "*.tsx" | grep -v "src/features/received/config/index.ts" | grep -v "test" | grep -v "mock"

검증 결과에 따라, 코드 수정이나 주석 추가 여부를 결정해 주시기 바랍니다.

src/features/received/ui/receivedCard.tsx (1)

70-70: 접근성 개선을 위한 적절한 변경

alt 속성을 "f"에서 ""로 변경한 것은 접근성 측면에서 적절한 변경입니다. 아바타 이미지는 이미 옆에 사용자 정보가 텍스트로 표시되고 있으므로, 스크린 리더가 중복된 정보를 읽지 않도록 빈 alt 속성을 사용하는 것이 더 좋습니다.

src/shared/ui/Toast/index.tsx (1)

18-18: 컴포넌트 유연성 감소 확인

props와 관련 기능을 제거함으로써 컴포넌트의 유연성이 감소했습니다. 이것이 의도적인 변경인지 확인하고, 필요한 경우 특정 커스터마이징이 필요한 상황에 대비해 일부 props를 다시 추가하는 것을 고려해 보세요.

src/app/(received)/received/interesting/page.tsx (3)

3-3: useEffect 사용에 대한 추가 import가 적절합니다.
추가된 useEffect import는 컴포넌트 내에서 사이드 이펙트를 처리하기에 적합하며, 코드를 명확하게 유지해 줍니다.


5-6: 새로운 훅 import 구조가 깔끔합니다.
useReceivedCardsQuery, useReceivedCardsStore 모두 명확하게 목적과 용도가 드러나며, 모듈화가 잘 된 것으로 보입니다.


15-16: 쿼리 결과를 의미 있는 변수명으로 파악하기 좋습니다.
serverReceivedCards라는 명칭은 API에서 수신된 데이터라는 사실을 명확히 표현해 주며, useReceivedCardsStoresetReceivedCards와도 연계가 잘 되어 있습니다.

src/features/received/ui/receivedCardView.tsx (13)

2-2: React 훅 사용 준비가 잘 되어 있습니다.
useState, useEffect를 함께 사용하는 구조가 명확해 보입니다.


12-15: 폴더 관련 훅 및 스토어 임포트가 명확합니다.
생성, 삭제, 수정 로직을 분리한 점이 유지보수에 유리해 보입니다.


17-19: 재사용성 있는 컴포넌트 임포트 구조가 좋습니다.
FoldersListReceivedCardList로 명확하게 UI를 나눈 점이 가독성을 높여줍니다.


21-24: 컴포넌트 Props 타입 정의가 명확합니다.
selectedFolderIdsetSelectedFolderId를 명시해주어 컴포넌트 사용 시 혼동이 적어집니다.


35-39: 스토어에서 가져온 함수와 서버 mutation을 함께 관리해주는 방식이 합리적입니다.
동일한 동작을 클라이언트 상태와 서버 API 모두에서 처리할 수 있어 데이터 동기화 측면에서 유익합니다.


41-43: 폴더 선택 로직이 간결합니다.
handleFolderSelectselectedFolderId를 셋팅하는 방식이 뚜렷합니다.


45-47: 새 폴더 추가 모달을 열기 위한 함수가 명확합니다.
isAdd 상태를 바로 세팅해주어 의도가 한눈에 들어옵니다.


48-51: 폴더 수정 시 폴더명을 상태에 저장하는 방식
folder.name을 key로 삼을 경우, 동일한 이름의 폴더가 여러 개 있을 때 의도치 않은 폴더를 수정하게 될 수 있습니다. 폴더 ID를 활용하는 방법도 검토해주세요.


52-57: 폴더 삭제 로직 구현
서버 요청과 스토어 삭제가 일관성 있게 처리되고, 완료 후 toast로 알림을 주어 UX가 좋습니다.


59-71: handleUpdateKeyDown 로직의 폴더명 검색 기준 확인 필요
folderName이 중복되면 findIndex로 정확한 폴더를 찾지 못할 가능성이 있으니, ID 기반으로 찾도록 개선이 필요할 수 있습니다.


84-89: 모달 닫힐 때 상태 초기화는 적절합니다.
isModalOpen이 닫히면 isAddisUpdatefalse로 돌리는 처리가 간단 명료합니다.


104-104: FoldersList를 통한 폴더 선택 구조가 일관성 있습니다.
UI 로직과 데이터 로직이 분리되어 있어 확장에 유리해 보입니다.


115-115: 새로운 ReceivedCardList 컴포넌트와 잘 연동됩니다.
selectedFolderId를 직접 받아 해당 폴더에 맞는 데이터만 표시할 수 있어, 코드 조직이 깔끔합니다.

src/app/(received)/received/page.tsx (9)

3-4: 라우터 훅과 React 훅의 도입이 적절합니다.
페이지 이동 및 상태 관리를 위해 useRouter, useState를 함께 사용하는 구조가 효과적으로 보입니다.


6-9: 쿼리와 스토어를 한 파일에서 임포트하여 일관성이 좋습니다.
useFoldersQuery, useReceivedCardsQuery, useFolderStore, useReceivedCardsStore 등 필요한 모듈을 한눈에 확인 가능해 유지보수에 유리합니다.


19-19: 폴더 선택 상태 추가
selectedFolderId 상태를 통해 폴더 기반으로 카드를 필터링 가능해, UI가 유연해졌습니다.


20-21: 멀티 쿼리 사용으로 폴더와 카드 정보를 분리
isCardsLoading, isFoldersLoading 상태를 활용해 로딩 처리도 명확합니다.


23-23: 폴더 선택 모달 상태 분리
isChooseModalOpen을 모달에 넘겨주는 구조가 단순해, 관리하기에 편리해 보입니다.


25-26: 스토어에 폴더와 명함 데이터를 저장하는 방식
API로부터 받은 값을 redux-like 스토어에 재할당하여 전역 상태 관리가 깔끔해졌습니다.


37-46: 폴더 선택 시 refetch 로직
폴더 변경 시 마다 데이터를 새로 가져와 동기화하는 것은 유연하지만, 빈번한 refetch 로 인한 퍼포먼스 영향을 확인해 보시는 것이 좋겠습니다.


53-53: Appbar의 선택 콜백 연결
onRightClickSecond를 통해 명함 선택 모달을 열 수 있어 사용자 흐름이 분명합니다.


55-55: ReceivedCardView에 폴더 선택 상태를 넘기는 구조
selectedFolderId를 하위 컴포넌트에서 직접 사용할 수 있어, 관련 코드가 깔끔하고 예측 가능해집니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

useDeleteFolder, useCreateFolder, useEditFolder에서 request body를 전달하는 부분에서 로직이 다 달라요.. 사실 챗지피티의 도움을 받아서,, 각 로직의 차이점이 뭔지 잘 모르겠습니다. 어떨 때 어떤 로직을 쓰는지, 각 request body를 불러오는 로직을 잘 쓴 건지 리뷰해주시면 감사하겠습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

오 보통 useDeleteFolder 방식으로 사용할텐데 페칭하는 중에 문제는 없었나요?

Copy link
Collaborator

@jangwonyoon jangwonyoon Apr 1, 2025

Choose a reason for hiding this comment

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

@youngju6143

  1. 가장 추천하는 방법은 라이브러리의 mutationFn의 함수를 확인해보고 타입을 뜯어 보는 방법을 추천합니다.
  2. 현재 확인해보니 variables 명시적으로 선언해서 payload를 전달하는 방법과, 명시적으로 선언하는것이 아닌 제네릭 방법으로 전달하는 방법 즉 아래 코드로 혼동해서 사용한것으로 보입니다. 각 차이점은 스타일이라고 할수있겠는데요, 현재 적용한 부분에서 문제는 없습니다.

variables를 명시적으로 선언해서 사용

mutationFn: (variables: { folderId: number; name: string }) => _editFolder(variables.folderId, variables.name)

고유 제네릭 사용

mutationFn: (folderName: string) => _createFolder(folderName),

또한 이는 axios 함수에 인자를 전달하는 것에 따라 바뀔수도 있습니다.

개인적인 의견은 저는 제네릭 타입을 사용해서 payload를 그대로 전달해주는 방법을 선호합니다. payload의 필드가 많은 경우 명시적으로 선언해서 사용하는 방법은 비효율적이라 생각합니다.

ex)

const payload =  {
id:1,
a:1,
b:1,
c:1,
d:1,
e:1
}

위 처럼 페이로드로 전달해주여야 하는 값이 많은 경우 명시적으로 선언할때, 코드를 인용하자면 아래와 같이 사용해야합니다.

const _deleteFolder = async (id, a, b, c, d, e) => {
  try {
    const response = await client.delete(`${CLIENT_SIDE_URL}/api/card/folder`, { data: { id, a, b, c, d, e } });
    return response;
  } catch (error) {
    console.error('폴더 삭제 실패 :', error);
    throw error;
  }
};

export const useDeleteFolder = () => {
  return useMutation({
    // mutationFn: (folderId: number) => _deleteFolder(folderId),
    mutationFn: (variables: { id, a,b,c,d,e }) => _deleteFolder(variables.id, variables.a,variables.b,variables.c,variables.d,variables.e),
    onSuccess: () => {
      console.log('폴더 삭제 성공');
    },
    onError: (error) => {
      console.error('폴더 삭제 실패:', error);
    },
  });
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@semnil5202 넵 페칭하는 데에는 다른 문제 발생은 없었습니다..!

@jangwonyoon 확실히 쿼리와 뮤테이션에 대해 공부할 필요를 많이 느끼네요..ㅜ 알려주셔서 감사합니다 😄

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: 3

🧹 Nitpick comments (9)
src/features/received/ui/foldersList.tsx (2)

20-20: 임시 로딩 텍스트 개선이 필요합니다.

로딩 상태 UI는 임시로 구현된 것으로 보입니다. 실제 배포 전에 로딩 스피너나 스켈레톤과 같은 적절한 UI 컴포넌트로 대체하는 것이 좋겠습니다.


14-42: 폴더 목록이 비어있는 경우를 처리해주세요.

현재 코드는 폴더 목록이 비어있는 경우에 대한 처리가 없습니다. 폴더가 없을 때 사용자에게 안내 메시지를 보여주는 것이 UX 측면에서 좋습니다.

  if (isLoading || isFetching) return <p>폴더 로딩중이에요...</p>; // 임시 로딩 구현
+ if (folders.length === 0) return <p>폴더가 없습니다. 첫 폴더를 만들어보세요!</p>;
  return (
    ...
  );
src/features/received/model/queries/useDeleteFolder.ts (3)

6-14: 에러 로깅 중복을 제거하세요.

현재 _deleteFolder 함수와 useDeleteFolder 훅 모두에서 동일한 에러를 로깅하고 있습니다. 에러 로깅은 한 곳에서만 처리하는 것이 좋습니다.

const _deleteFolder = async (folderId: number) => {
  try {
    const response = await client.delete(`${CLIENT_SIDE_URL}/api/card/folder`, { data: { folderId } });
    return response;
  } catch (error) {
-   console.error('폴더 삭제 실패 :', error);
    throw error;
  }
};

22-22: 프로덕션 코드에서 불필요한 console.log를 제거하세요.

프로덕션 환경에서는 디버깅용 console.log를 제거하는 것이 좋습니다. 로깅이 필요한 경우 더 체계적인 로깅 시스템을 사용하세요.

  onSuccess: () => {
-   console.log('폴더 삭제 성공');
    queryClient.invalidateQueries({ queryKey: ['folders'] });
  },

19-28: Toast 메시지를 추가하세요.

폴더 삭제 성공/실패 시 사용자에게 Toast 메시지로 알려주는 것이 좋은 UX 입니다. PR에 Toast 컴포넌트가 이미 있는 것으로 보이니 활용해보세요.

export const useDeleteFolder = () => {
  const queryClient = useQueryClient();
+  const { showToast } = useToast(); // Toast 컴포넌트 hook 추가 필요

  return useMutation({
    mutationFn: (variables: { folderId: number }) => _deleteFolder(variables.folderId),
    onSuccess: () => {
      console.log('폴더 삭제 성공');
      queryClient.invalidateQueries({ queryKey: ['folders'] });
+     showToast('폴더가 삭제되었습니다.');
    },
    onError: (error) => {
      console.error('폴더 삭제 실패:', error);
+     showToast('폴더 삭제에 실패했습니다. 다시 시도해주세요.');
    },
  });
};
src/features/received/model/queries/useFoldersQuery.ts (2)

10-10: 불필요한 console.log를 제거하세요.

주석에도 언급되어 있듯이 '추후 지우기'로 표시된 console.log가 있습니다. 이런 디버깅용 코드는 PR 병합 전에 제거하는 것이 좋습니다.

  try {
    const { data } = await client.get<FolderDto>(`${CLIENT_SIDE_URL}/api/card/folders`);
-   console.log('서버로부터 받은 데이터 : ', data); // 추후 지우기
    return data;
  } catch (error) {

18-25: useFoldersQuery 리팩토링 제안

현재 구현은 이미 개선되었지만, 더 간결하게 만들 수 있습니다. 특히 폴더 데이터가 없을 때의 기본값 처리와 쿼리 옵션을 더 명확하게 할 수 있습니다.

export const useFoldersQuery = () => {
  const { data, isLoading, isFetching, isError, refetch } = useQuery({
    queryKey: ['folders'],
    queryFn: _getFolders,
    enabled: true,
+   staleTime: 1000 * 60 * 5, // 5분 동안 데이터 캐싱
+   retry: 1, // 실패 시 한 번만 재시도
  });
  return { folders: data?.folders ?? [], isLoading, isFetching, isError, refetch };
};
src/app/(received)/received/page.tsx (2)

34-34: 불필요한 console.log를 제거하세요.

디버깅용 console.log는 PR 병합 전에 제거하는 것이 좋습니다.

useEffect(() => {
  if (!isFoldersLoading) setFolders(serverFolders);
-  console.log('선택한 폴더 : ', selectedFolderId);
}, [isFoldersLoading]);

41-50:

❓ Verification inconclusive

폴더 선택 시 카드 데이터 refetch 최적화

모든 폴더 선택에서 API 호출이 발생하는 것은 좋습니다만, 이미 React Query가 selectedFolderId가 변경될 때 자동으로 refetch 하도록 설정되어 있습니다. 불필요한 추가 refetch가 발생할 수 있습니다.

useReceivedCardsQuery 훅에 selectedFolderId가 의존성으로 올바르게 설정되어 있는지 확인해 주세요. 이 값이 변경되면 자동으로 쿼리가 다시 실행되어야 합니다.


🏁 Script executed:

#!/bin/bash
# useReceivedCardsQuery 훅의 구현을 확인합니다.
rg -A 5 "useReceivedCardsQuery" --type ts --type tsx

Length of output: 80


리팩토링 요청: useReceivedCardsQuery 훅의 의존성 배열 재검증 필요

현재 페이지의 useEffect에서 별도로 API를 호출해 카드를 refetch 하고 있지만, React Query가 제공하는 useReceivedCardsQuery 훅이 selectedFolderId 변경 시 자동으로 쿼리를 재실행하도록 구성되어 있다면 중복 refetch가 발생할 수 있습니다.
따라서 아래 사항을 확인해 주세요:

  • useReceivedCardsQuery 훅 내에 selectedFolderId가 의존성 배열에 포함되어 있는지
    → 만약 누락되어 있다면, selectedFolderId가 변경될 때 자동 refetch가 안 되어 추가로 API 호출이 발생할 수 있습니다.
  • 관련 구현을 다시 검토하시고, 필요 시 해당 의존성을 추가하는 방식으로 리팩토링 해주시기 바랍니다.
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 090ff2a and d9595e7.

📒 Files selected for processing (6)
  • src/app/(received)/received/page.tsx (1 hunks)
  • src/features/received/model/queries/useCreateFolder.ts (1 hunks)
  • src/features/received/model/queries/useDeleteFolder.ts (1 hunks)
  • src/features/received/model/queries/useEditFolder.ts (1 hunks)
  • src/features/received/model/queries/useFoldersQuery.ts (1 hunks)
  • src/features/received/ui/foldersList.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/features/received/model/queries/useEditFolder.ts
  • src/features/received/model/queries/useCreateFolder.ts
🧰 Additional context used
🧬 Code Definitions (1)
src/features/received/ui/foldersList.tsx (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • useFoldersQuery (18-25)
🔇 Additional comments (1)
src/features/received/model/queries/useDeleteFolder.ts (1)

1-29:

❓ Verification inconclusive

mutation 함수의 요청 본문(request body) 전달 방식이 일관적이지 않습니다.

이전 리뷰 코멘트에서 언급된 대로, 각 mutation 함수마다 요청 본문을 전달하는 방식이 다릅니다. 이 파일에서는 data: { folderId } 형태로 전달하고 있는데, 다른 파일들과 일관성 있게 통일하는 것이 좋겠습니다.


🌐 Web query:

axios HTTP 요청 본문을 DELETE 메소드에서 어떻게 전달하나요?

💡 Result:

Axios를 사용하여 DELETE 메서드로 HTTP 요청 본문을 전달하려면 config 객체의 data 속성을 활용해야 합니다. DELETE 요청은 POST나 PUT과 달리 기본적으로 두 번째 파라미터로 데이터를 직접 전달하지 않으며, 명시적으로 data 속성에 객체를 할당해야 서버에서 req.body로 접근 가능합니다.

1. 기본 방법: axios.delete() 활용

axios.delete() 메서드의 두 번째 인자로 data 속성을 포함한 설정 객체를 전달합니다.

axios.delete('/api/resource', {
  data: {
    id: 123,
    password: 'securePassword123'
  },
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => console.log(response))
.catch(error => console.error(error));
  • 주의사항: 서버 측에서 req.body 파싱을 위해 body-parser 미들웨어가 구성되어 있어야 합니다[1][4][10].

2. 확장 방법: 일반 Axios API 사용

method 속성을 명시적으로 지정하는 방식으로도 구현 가능합니다.

axios({
  method: 'delete',
  url: '/api/resource',
  data: {
    id: 456,
    token: 'abcde-xyz'
  }
})
.then(response => console.log(response))
.catch(error => console.error(error));

주요 차이점 비교

방법 구문 예시 사용 사례
axios.delete() 명시적 DELETE 메서드 호출 단순한 DELETE 요청
일반 Axios API 유연한 설정 객체 사용 복잡한 요청 구성이 필요할 때

HTTP 표준에서는 DELETE 메서드에 본문 사용을 권장하지 않지만, Axios는 data 속성을 통해 본문 전송을 지원합니다[13][16]. 단, 서버 측 라우트에서 DELETE 요청의 본문을 처리할 수 있도록 구현해야 합니다 (Express 예시: app.delete('/path', (req, res) => { ... })).

실무에서는 RESTful 원칙에 따라 리소스 식별자를 URL 경로나 쿼리 파라미터로 전달하는 것이 일반적이지만, 민감한 데이터 전송 시 본문 사용이 유용할 수 있습니다[6][13].

Citations:


일관성 체크 필요 – DELETE 요청 본문 전달 방식 통일

현재 useDeleteFolder 훅 내의 _deleteFolder 함수는 axios의 DELETE 메서드 요구사항에 맞게 { data: { folderId } } 형태로 요청 본문을 전달하고 있어 기술적으로는 올바른 구현입니다. 다만, 다른 mutation 함수들과 전달 방식이 상이한 것으로 보이므로 전체 코드베이스 내에서 요청 본문 전달 방식의 통일성을 검토 및 수정할 필요가 있습니다.

  • 확인 사항
    • 다른 mutation 함수들이 DELETE 혹은 기타 메서드에서 요청 본문을 전달하는 방식이 어떻게 구현되어 있는지 검토
    • 내부 컨벤션에 따라 수정할지, 아니면 전역적으로 기존 방식(axios의 data 속성 사용)을 채택할지 결정

이 부분은 다른 파일과의 일관성을 고려하여 추가 검토 후 통일된 구현으로 수정해 주시기 바랍니다.

Comment on lines +17 to +20
const { isLoading, isFetching } = useFoldersQuery();
const { folders } = useFolderStore();

if (isLoading || isFetching) return <p>폴더 로딩중이에요...</p>; // 임시 로딩 구현
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 오류가 발생할 경우에 대한 처리가 없습니다. isError 상태를 확인하여 오류 메시지를 표시하는 로직을 추가하는 것이 좋습니다.

- const { isLoading, isFetching } = useFoldersQuery();
+ const { isLoading, isFetching, isError } = useFoldersQuery();
  const { folders } = useFolderStore();

  if (isLoading || isFetching) return <p>폴더 로딩중이에요...</p>; // 임시 로딩 구현
+ if (isError) return <p>폴더를 불러오는 데 실패했습니다. 다시 시도해주세요.</p>;
  return (
    ...
  );
📝 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 { isLoading, isFetching } = useFoldersQuery();
const { folders } = useFolderStore();
if (isLoading || isFetching) return <p>폴더 로딩중이에요...</p>; // 임시 로딩 구현
const { isLoading, isFetching, isError } = useFoldersQuery();
const { folders } = useFolderStore();
if (isLoading || isFetching) return <p>폴더 로딩중이에요...</p>; // 임시 로딩 구현
if (isError) return <p>폴더를 불러오는 데 실패했습니다. 다시 시도해주세요.</p>;
return (
...
);

Copy link
Collaborator

@semnil5202 semnil5202 left a comment

Choose a reason for hiding this comment

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

고생많으셨습니다. 쿼리 다루는 부분에서 잘못 작성된 부분이 좀 있어서 코멘트 남겼습니다.

이 부분 확인해주시고 수정 후에 머지하면 좋을 것 같아요.

#149 (comment) 이 코멘트와 #149 (comment) 이 코멘트가 서로 연관이 있어보입니다.

Comment on lines 49 to 54
useEffect(() => {
if (!isFetching) {
setReceivedCards(cards);
}
console.log('asdfasdfa');
}, [isFetching]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

음 react query로 받아온 서버상태 (cards)를 클라이언트 상태 (selectedCardIds)로 바꿀 이유가 딱히 없어보이는데, 이런 로직이 추가된 배경이 있을까요?

콘솔도 지워주세요~

Copy link
Collaborator

Choose a reason for hiding this comment

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

@semnil5202 이 부분은 @youngju6143 의견을 들어봐야겠지만 전역상태로 관리하고 싶어서 사용한게 아닌가 싶습니다.

그것과 별개로 isFetching에 플래그를 두어 할당한 부분이 어색해 보입니다. 실제 데이터가 있는경우 즉 data가 undefined가 아닌 경우에 할당이 되도록 다루는게 좋아보입니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@semnil5202 폴더를 이동할 때, 여러 개의 카드 ID를 선택해야 해서 클라이언트 상태에서 selectedCardIds 배열을 관리하고 있습니다.
이후, 폴더를 이동하거나 삭제할 때 이 배열을 request body로 전달하는 방식으로 구현했습니다.

현재 로직이 가장 적절하다고 판단했는데, 혹시 더 나은 방법이 있을까요?

Copy link
Collaborator

@jangwonyoon jangwonyoon left a comment

Choose a reason for hiding this comment

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

@youngju6143 고생하셨습니다. 몇 가지 코멘트를 남겨두었습니다~

const { data, isLoading, isFetching, isError, refetch } = useQuery({
queryKey: ['cards'],
queryFn: () => _getReceivedCards(folderId),
enabled: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

enabled의 값은 default값이 true입니다. 현재 상황에서는 folderId 값이 존재하는 경우에만 사용하도록

enabled: !!folderId

로 사용하는것이 맞아보입니다.

Copy link
Collaborator

@semnil5202 semnil5202 Apr 1, 2025

Choose a reason for hiding this comment

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

folderId 가 초반에 undefined여서 fetching 에러가 나기때문에 위와같이 작업합니다. Dependecy Query 키워드로 검색해보면 관련 내용을 볼 수 있습니다.

Comment on lines 49 to 54
useEffect(() => {
if (!isFetching) {
setReceivedCards(cards);
}
console.log('asdfasdfa');
}, [isFetching]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@semnil5202 이 부분은 @youngju6143 의견을 들어봐야겠지만 전역상태로 관리하고 싶어서 사용한게 아닌가 싶습니다.

그것과 별개로 isFetching에 플래그를 두어 할당한 부분이 어색해 보입니다. 실제 데이터가 있는경우 즉 data가 undefined가 아닌 경우에 할당이 되도록 다루는게 좋아보입니다.

@JaeIn1
Copy link
Collaborator

JaeIn1 commented Apr 1, 2025

@youngju6143 너무 고생하셨습니다~ 👍 👍

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: 1

🧹 Nitpick comments (9)
src/features/received/model/mutations/useDeleteReceivedCard.ts (3)

8-16: 에러 처리가 중복되어 있습니다.

_deleteReceivedCards 함수와 useMutationonError 콜백 모두에서 에러 로깅이 이루어지고 있습니다. 더 깔끔한 코드를 위해 한 곳에서만 로깅하는 것을 고려해보세요.

const _deleteReceivedCards = async (cardIds: number[]) => {
  try {
    const response = await client.delete(`${CLIENT_SIDE_URL}/api/card/receive`, { data: { cardIds } });
    return response;
  } catch (error) {
-   console.error('받은 명함 삭제 실패 :', error);
    throw error;
  }
};

21-30: useMutation 구성이 적절합니다만, 사용자 피드백을 위한 개선이 필요합니다.

mutation 함수와 콜백이 잘 구성되어 있지만, 사용자에게 성공 또는 실패 메시지를 표시하는 방법이 없습니다. 토스트 메시지나 알림을 통해 사용자에게 작업 결과를 알려주는 것이 좋습니다.

export const useDeleteReceivedCards = (folderId: number | null) => {
  const queryClient = useQueryClient();
+  const { showToast } = useToast(); // 토스트 훅 추가

  return useMutation({
    mutationFn: (variables: { cardIds: number[] }) => _deleteReceivedCards(variables.cardIds),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [CARD_QUERY_KEY, folderId] });
-      console.log('받은 명함 삭제 성공');
+      showToast({ message: '명함이 성공적으로 삭제되었습니다.', type: 'success' });
    },
    onError: (error) => {
      console.error('받은 명함 삭제 실패:', error);
+      showToast({ message: '명함 삭제에 실패했습니다. 다시 시도해주세요.', type: 'error' });
    },
  });
};

1-32: 전체적으로 코드가 잘 구성되어 있습니다.

이 훅은 받은 명함 삭제 기능을 효과적으로 구현하고 있습니다. React Query를 활용한 데이터 관리가 적절하게 이루어지고 있으며, API 호출 및 쿼리 무효화 로직이 깔끔하게 정리되어 있습니다.

다음과 같은 작은 개선을 고려해볼 수 있습니다:

  1. 응답 및 에러에 대한 타입 정의 추가
  2. 성공/실패 시 사용자 피드백 메커니즘 추가
  3. cardIds 배열의 유효성 검사 (예: 빈 배열 체크)
src/features/received/model/queries/useFoldersQuery.ts (1)

12-12: 개발 완료 후 console.log 제거 필요

추후 제거하겠다고 표시해두셨으나, 프로덕션 환경에 배포하기 전에 콘솔 로그를 반드시 제거해주세요. 디버깅용 로그는 개발 중에는 유용하지만 프로덕션에서는 성능 이슈와 보안 위험을 초래할 수 있습니다.

src/features/received/model/mutations/useEditFolder.ts (1)

10-13: ES6 객체 속성 단축 구문 적용 권장

객체 속성 이름과 변수 이름이 동일할 때는 ES6의 단축 구문을 사용하면 코드를 더 간결하게 작성할 수 있습니다:

-    const response = await client.put(`${CLIENT_SIDE_URL}/api/card/folder`, {
-      folderId: folderId,
-      name: name,
-    });
+    const response = await client.put(`${CLIENT_SIDE_URL}/api/card/folder`, {
+      folderId,
+      name,
+    });

이렇게 하면 코드가 더 간결해지고 가독성이 향상됩니다.

src/features/received/model/mutations/useCreateFolder.ts (2)

25-28: 콘솔 로그 제거 필요

성공 메시지를 콘솔에 출력하는 것은 개발 중에는 유용하지만, 프로덕션에 배포하기 전에는 제거하거나 적절한 로깅 시스템으로 대체하는 것이 좋습니다. 또는 사용자에게 토스트 메시지나 알림을 표시하는 방식으로 변경하는 것이 좋습니다.

  onSuccess: () => {
-    console.log('폴더 생성 성공');
    queryClient.invalidateQueries({ queryKey: [FOLDER_QUERY_KEY] });
+    // 필요하다면 토스트 메시지 표시
  },

1-33: 유사한 mutation 파일들에 대한 리팩토링 고려

이 파일과 다른 mutation 파일들(useEditFolder, useDeleteFolder)이 매우 유사한 패턴을 가지고 있습니다. 향후 유지보수를 위해 이러한 패턴을 추상화하여 중복을 줄이는 것을 고려해보세요. 예를 들어, 공통 mutation 생성 함수를 만들고 각 액션에 맞게 구성할 수 있습니다:

// 예시: createMutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { client } from '@/shared/apis/client';
import { CLIENT_SIDE_URL } from '@/shared/constants';

export const createMutation = ({
  mutationFn,
  queryKey,
  successMessage,
  errorMessage,
}) => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn,
    onSuccess: () => {
      console.log(successMessage);
      queryClient.invalidateQueries({ queryKey });
    },
    onError: (error) => {
      console.error(errorMessage, error);
    },
  });
};

이런 식으로 중복 코드를 줄이고 유지보수성을 높일 수 있습니다.

src/features/received/model/mutations/useMoveCardToFolder.ts (2)

6-17: API 요청 함수가 잘 구현되어 있지만, 오류 처리를 개선할 수 있습니다.

_moveCardToFolder 함수는 기본적인 구조가 잘 잡혀 있습니다. 하지만 다음과 같은 개선점이 있습니다:

  1. 오류 처리가 모든 오류 유형에 대해 동일하게 처리되고 있습니다. API 응답 코드나 네트워크 오류 등 다양한 상황에 맞게 처리하는 것이 좋습니다.
  2. 콘솔 로그는 개발 환경에서만 표시되도록 조건부로 처리하는 것이 좋습니다.
const _moveCardToFolder = async (folderId: number, cardIds: number[]) => {
  try {
    const response = await client.put(`${CLIENT_SIDE_URL}/api/card/receive/folder`, {
      folderId: folderId,
      cardIds: cardIds,
    });
    return response;
  } catch (error) {
-    console.error('받은 명함을 폴더에 저장 실패 :', error);
+    // 개발 환경에서만 로깅
+    if (process.env.NODE_ENV !== 'production') {
+      console.error('받은 명함을 폴더에 저장 실패 :', error);
+    }
+    // 오류 타입에 따른 처리
+    if (error instanceof Error) {
+      // 구체적인 오류 처리
+    }
    throw error;
  }
};

1-30: 타입 안전성 및 코드 구조 개선 제안

전체적으로 코드가 잘 작성되어 있지만, 다음과 같은 개선 사항을 고려해 보세요:

  1. API 응답 타입을 명시적으로 정의하면 타입 안전성이 향상됩니다.
  2. 변수를 객체 구조 분해 할당으로 전달하면 가독성이 향상될 수 있습니다.
  3. 재사용 가능한 에러 처리 유틸리티를 사용하는 것이 좋습니다.
import { useMutation } from '@tanstack/react-query';

import { client } from '@/shared/apis/client';
import { CLIENT_SIDE_URL } from '@/shared/constants';
+ import { queryClient } from '@/shared/apis/queryClient';
+ import { QUERY_KEYS } from '@/shared/constants/queryKeys';
+ import { handleApiError } from '@/shared/utils/errorHandling';
+ import { toast } from '@/shared/components/toast';

+ // API 응답 타입 정의
+ interface MoveCardResponse {
+   success: boolean;
+   message?: string;
+ }

- const _moveCardToFolder = async (folderId: number, cardIds: number[]) => {
+ const _moveCardToFolder = async ({ folderId, cardIds }: { folderId: number; cardIds: number[] }): Promise<MoveCardResponse> => {
  try {
    const response = await client.put<MoveCardResponse>(`${CLIENT_SIDE_URL}/api/card/receive/folder`, {
      folderId,
      cardIds,
    });
    return response;
  } catch (error) {
-    console.error('받은 명함을 폴더에 저장 실패 :', error);
+    handleApiError(error, '받은 명함을 폴더에 저장 실패');
    throw error;
  }
};

export const useMoveCardToFolder = () => {
  return useMutation({
-    mutationFn: (variables: { folderId: number; cardIds: number[] }) =>
-      _moveCardToFolder(variables.folderId, variables.cardIds),
+    mutationFn: (variables: { folderId: number; cardIds: number[] }) => _moveCardToFolder(variables),
    onSuccess: () => {
-      console.log('받은 명함을 폴더에 저장 성공');
+      queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.RECEIVED_CARDS] });
+      queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FOLDERS] });
+      toast.success('명함이 성공적으로 이동되었습니다.');
    },
    onError: (error) => {
-      console.error('받은 명함을 폴더에 저장 실패 :', error);
+      toast.error('명함 이동 중 오류가 발생했습니다.');
    },
  });
};
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9595e7 and 6eb611e.

📒 Files selected for processing (7)
  • src/features/received/model/mutations/useCreateFolder.ts (1 hunks)
  • src/features/received/model/mutations/useDeleteFolder.ts (1 hunks)
  • src/features/received/model/mutations/useDeleteReceivedCard.ts (1 hunks)
  • src/features/received/model/mutations/useEditFolder.ts (1 hunks)
  • src/features/received/model/mutations/useMoveCardToFolder.ts (1 hunks)
  • src/features/received/model/queries/useFoldersQuery.ts (1 hunks)
  • src/features/received/model/queries/useReceivedCardsQuery.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/received/model/queries/useReceivedCardsQuery.ts
🧰 Additional context used
🧬 Code Definitions (4)
src/features/received/model/mutations/useDeleteFolder.ts (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • FOLDER_QUERY_KEY (7-7)
src/features/received/model/mutations/useDeleteReceivedCard.ts (1)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • CARD_QUERY_KEY (7-7)
src/features/received/model/mutations/useEditFolder.ts (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • FOLDER_QUERY_KEY (7-7)
src/features/received/model/mutations/useCreateFolder.ts (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • FOLDER_QUERY_KEY (7-7)
🔇 Additional comments (13)
src/features/received/model/mutations/useDeleteReceivedCard.ts (5)

1-6: import 문이 효율적으로 구성되어 있습니다.

필요한 의존성이 적절하게 가져와져 있으며, CARD_QUERY_KEY를 활용하여 쿼리 캐시 무효화를 위한 준비가 잘 되어 있습니다.


10-10: API 요청 방식이 적절합니다.

DELETE 요청에 request body를 포함시키는 방식이 올바르게 구현되어 있습니다. { data: { cardIds } } 형태로 데이터를 전달하는 것은 axios에서 DELETE 요청에 body를 포함할 때 권장되는 방식입니다.


18-20: folderId의 타입과 역할이 명확합니다.

folderIdnumber | null 타입으로 정의되어 있어, 특정 폴더 또는 모든 폴더에 대한 쿼리 무효화를 처리할 수 있습니다.


24-24: 쿼리 무효화 전략이 적절합니다.

특정 폴더의 명함 데이터를 삭제한 후, 해당 폴더의 쿼리만 무효화하는 전략이 효율적입니다. 불필요한 데이터 리페칭을 방지하고 성능을 최적화합니다.


22-22: mutationFn의 인터페이스가 명확합니다.

variables 객체를 통해 cardIds 배열을 받아 내부 함수로 전달하는 패턴이 잘 구현되어 있습니다. 사용 시 타입 안전성을 제공합니다.

src/features/received/model/queries/useFoldersQuery.ts (3)

21-24: 쿼리 키 관리 개선 권장

이전 리뷰에서 언급된 것처럼, 쿼리 키를 상수로 관리하는 것이 좋습니다. 현재는 단일 상수가 있지만, 더 구조화된 방식으로 관리하면 invalidation 시 일관성을 유지하기 쉽습니다.

-export const FOLDER_QUERY_KEY = 'FOLDER_QUERY_KEY';
+export const QUERY_KEYS = {
+  FOLDERS: 'folders',
+} as const;

그리고 사용할 때:

-queryKey: [FOLDER_QUERY_KEY],
+queryKey: [QUERY_KEYS.FOLDERS],

이렇게 하면 추후 다른 쿼리 키를 추가할 때 더 일관된 구조를 유지할 수 있습니다.


9-18: 에러 처리와 구조화가 잘 되어 있습니다

비동기 함수에 try-catch 블록을 추가하여 에러 처리를 개선한 점이 좋습니다. 에러를 적절히 기록하고 다시 throw하여 상위 컴포넌트에서 처리할 수 있게 한 부분도 좋은 패턴입니다.


26-26: 빈 배열 기본값 설정이 잘 되어 있습니다

folders: data?.folders ?? []와 같이 옵셔널 체이닝과 nullish 병합 연산자를 사용하여 기본값을 빈 배열로 설정한 것은 좋은 방식입니다. 이를 통해 데이터가 없는 경우에도 컴포넌트가 오류 없이 렌더링될 수 있습니다.

src/features/received/model/mutations/useDeleteFolder.ts (2)

8-16: DELETE 요청의 구현 방식에 대한 참고사항

이 구현은 잘 작동하지만, REST 관행에 따르면 일반적으로 DELETE 요청은 요청 본문(body)보다는 URL 파라미터를 통해 ID를 전달합니다 (예: /api/card/folder/{folderId}). 현재 방식은 백엔드 요구사항에 맞춘 것으로 보이므로 작동에는 문제가 없지만, REST 표준과는 약간 다른 접근 방식임을 알려드립니다.


18-31: mutationFn과 onSuccess/onError 핸들링이 잘 구현되어 있습니다

queryClient를 통해 성공 시 쿼리를 무효화하는 로직이 잘 구현되어 있습니다. 이렇게 하면 폴더 삭제 후 UI가 최신 상태로 자동 업데이트됩니다. 또한 에러 핸들링도 적절히 구현되어 있어서, 에러 발생 시 사용자에게 더 나은 피드백을 제공할 수 있습니다.

src/features/received/model/mutations/useEditFolder.ts (1)

21-34: 쿼리 무효화 및 에러 처리가 잘 구현되어 있습니다

성공 시 적절한 쿼리 무효화와 에러 발생 시 에러 로깅이 잘 구현되어 있습니다. 이는 사용자 경험을 향상시키는 데 중요한 패턴입니다. 필요한 경우 추가적으로 토스트 메시지나 알림을 통해 사용자에게 작업 결과를 피드백하는 것도 고려해 볼 수 있습니다.

src/features/received/model/mutations/useCreateFolder.ts (1)

8-18: API 호출 및 에러 처리가 잘 구현되어 있습니다

비동기 함수에 try-catch 블록을 사용하여 에러를 적절히 처리하고 있습니다. 또한 에러를 콘솔에 기록하고 상위로 다시 throw하여 호출자가 처리할 수 있게 한 것도 좋은 방식입니다.

src/features/received/model/mutations/useMoveCardToFolder.ts (1)

1-5: 필요한 import 구문이 잘 구성되어 있습니다.

import 구문이 깔끔하게 정리되어 있고 필요한 라이브러리와 내부 모듈만 가져오고 있습니다.

Comment on lines 19 to 30
export const useMoveCardToFolder = () => {
return useMutation({
mutationFn: (variables: { folderId: number; cardIds: number[] }) =>
_moveCardToFolder(variables.folderId, variables.cardIds),
onSuccess: () => {
console.log('받은 명함을 폴더에 저장 성공');
},
onError: (error) => {
console.error('받은 명함을 폴더에 저장 실패 :', error);
},
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

mutation 훅에 쿼리 무효화 및 중복 오류 처리 개선이 필요합니다.

현재 useMoveCardToFolder 훅에는 다음과 같은 개선 사항이 필요합니다:

  1. 내부 함수와 onError 콜백에서 오류 로깅이 중복됩니다.
  2. 명함 이동 후 관련 쿼리를 무효화(invalidate)하여 데이터를 새로고침해야 합니다.
  3. 사용자에게 피드백을 제공하는 메커니즘이 없습니다(토스트 메시지 등).
  4. 반환 값에 대한 타입 정의가 명시적이면 더 좋을 것 같습니다.
+ import { queryClient } from '@/shared/apis/queryClient';
+ import { QUERY_KEYS } from '@/shared/constants/queryKeys';
+ import { toast } from '@/shared/components/toast';

export const useMoveCardToFolder = () => {
  return useMutation({
    mutationFn: (variables: { folderId: number; cardIds: number[] }) =>
      _moveCardToFolder(variables.folderId, variables.cardIds),
    onSuccess: () => {
-      console.log('받은 명함을 폴더에 저장 성공');
+      // 관련 쿼리 무효화
+      queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.RECEIVED_CARDS] });
+      queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FOLDERS] });
+      // 사용자 피드백
+      toast.success('명함이 성공적으로 이동되었습니다.');
    },
    onError: (error) => {
-      console.error('받은 명함을 폴더에 저장 실패 :', error);
+      // 사용자 피드백 (오류 로깅은 내부 함수에서 이미 처리)
+      toast.error('명함 이동 중 오류가 발생했습니다.');
    },
  });
};

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

🧹 Nitpick comments (15)
src/app/(received)/received/interesting/page.tsx (2)

18-20: useEffect 의존성 배열 검토
현재 isLoading, serverReceivedCards를 의존성 배열에 포함하여, 로딩 기간 중에만 setReceivedCards를 수행합니다. 로딩이 끝나도 재렌더가 필요하다면, 조건문을 좀 더 세분화하거나 isLoading === false 시점에만 동작하도록 조정해 볼 수도 있습니다.


27-27: 서버 데이터 매핑 로직
serverReceivedCards.map 는 정상적으로 데이터를 순회합니다. 다만, 카드에 식별자(id 등)가 있다면 index 대신 idkey에 사용하는 것을 권장합니다.

src/features/received/ui/receivedCardView.tsx (7)

2-2: 불필요한 상태 불러오기 확인
useEffect, useState를 모두 import하셨는데, 실제로 사용 중인지 다시 확인해 주세요. 사용하지 않는 훅이라면 제거해 간결성을 유지할 수 있습니다.


12-15: 폴더 관리자용 Mutation 훅 적절성
useCreateFolder, useDeleteFolder, useEditFolder 훅을 모아서 관리하는 구조가 유지보수에 용이합니다. 다만 폴더를 서버에서 가져오는 로직(useFoldersQuery)과의 관계도 명확히 구분되어야 합니다.


21-24: props 타입 명시
ReceivedCardViewProps는 명확히 정의되어 있습니다. 폴더 ID가 null일 경우 전체 보기, 숫자일 경우 해당 폴더만 보여주는 등, 타입에 따른 동작스펙을 주석으로 남겨주면 가독성이 높아집니다.


30-32: 폴더 이름 상태 분리
folderName, newFolderName, updatedFolderName 등 세 가지 상태가 혼재되어 있습니다. 의미 구분이 확실하면 상관없지만, 향후 유지보수 시 혼동할 여지가 있으므로 주석이나 네이밍 등으로 구분도를 높이는 걸 권장합니다.


37-39: 폴더 서버 Mutation 훅 사용
serverCreateFolder, serverEditFolder, serverDeleteFolder 등의 네이밍이 직관적입니다. 성공/실패 콜백이 필요한 경우, 해당 훅에 onSuccess/onError 옵션을 추가해 재사용성을 높일 수 있습니다.


95-99: 폴더 아이콘 클릭 시점에 대한 기능 확인
headerRightHandler();, console.log(folders); 로직은 디버깅용으로 보입니다. 프로덕션에서 필요 없다면 제거 권장합니다.


118-160: 모달 내부 구현 적절성
폴더 업데이트, 추가, 설정까지 모두 하나의 모달로 통합하였고, isUpdate / isAdd / 이도 아닐 때의 분기 처리가 간결합니다. 라벨, 인풋, 버튼 등의 UI가 의도대로 동작하는지 UX 테스트를 통해 한 번 더 확인하면 좋겠습니다.

src/app/(received)/received/page.tsx (6)

3-4: Next.js 라우팅 관련 Import
useRouterReact, useEffect, useState가 함께 사용되고 있습니다. 불필요한 훅이 없는지 확인해 보세요.


6-9: 폴더 및 받은 명함 쿼리 훅
useFoldersQuery, useReceivedCardsQuery, useFolderStore, useReceivedCardsStore가 한 파일에서 모두 선언되어 있어 데이터 흐름이 다소 복잡해 보일 수 있습니다. 유지보수를 위해 구조를 단순화하거나, 한눈에 알아볼 수 있는 주석을 달아두는 것도 방법입니다.


23-23: 모달 State 관리
isChooseModalOpen, openChooseModal, closeChooseModal는 네이밍과 역할이 명확해 보입니다. 다른 모달과 혼동되지 않도록 주석 등으로 구분해 두면 좋겠습니다.


30-30: 추가 의존성 포함 여부
혹시 serverFolders 외 다른 값도 이 useEffect에서 사용된다면, 모두 의존성 배열에 포함해야 하니 주의 부탁드립니다.


32-33: 명함 데이터 세팅 useEffect
isCardsLoading === false 시점에만 serverReceivedCardssetReceivedCards 하는 로직 역시 자연스럽습니다. 다만, 로딩 외에 에러 처리 등도 고려해 보시면 좋겠습니다.


41-41: Appbar에서 onRightClickSecond
Menu를 열기 위한 함수처럼 보입니다. 코드 유지보수를 위해 다른 props와 네이밍 일관성을 맞추는 것이 좋겠습니다. (onRightClick, onMenuClick 등)

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb611e and 140489e.

📒 Files selected for processing (5)
  • src/app/(received)/received/interesting/page.tsx (2 hunks)
  • src/app/(received)/received/page.tsx (1 hunks)
  • src/features/received/ui/choose/chooseReceivedCardView.tsx (1 hunks)
  • src/features/received/ui/receivedCardList.tsx (1 hunks)
  • src/features/received/ui/receivedCardView.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/features/received/ui/receivedCardList.tsx
  • src/features/received/ui/choose/chooseReceivedCardView.tsx
🧰 Additional context used
🧠 Learnings (1)
src/app/(received)/received/interesting/page.tsx (2)
Learnt from: semnil5202
PR: depromeet/Took-FE#149
File: src/app/(received)/received/interesting/page.tsx:18-20
Timestamp: 2025-03-31T22:59:17.224Z
Learning: React useEffect 훅에서는 내부에서 사용하는 모든 변수(특히 props나 state)를 의존성 배열에 포함해야 합니다. 단, Zustand와 같은 상태 관리 라이브러리에서 제공하는 setter 함수는 안정적이므로 의존성 배열에 포함하지 않아도 됩니다.
Learnt from: semnil5202
PR: depromeet/Took-FE#149
File: src/app/(received)/received/choose/page.tsx:18-20
Timestamp: 2025-03-31T22:59:18.554Z
Learning: React useEffect Hook을 사용할 때는 내부에서 사용하는 모든 값(변수, 함수 등)을 의존성 배열에 포함시켜야 합니다. 그렇지 않으면 해당 값이 변경되었을 때 effect가 실행되지 않아 예상치 못한 버그가 발생할 수 있습니다.
🧬 Code Definitions (2)
src/app/(received)/received/interesting/page.tsx (1)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (27-35)
src/features/received/ui/receivedCardView.tsx (3)
src/features/received/model/store/useFoldersStore.ts (1)
  • useFolderStore (13-37)
src/features/received/ui/foldersList.tsx (1)
  • FoldersList (14-42)
src/features/received/ui/receivedCardList.tsx (1)
  • ReceivedCardList (12-24)
🔇 Additional comments (24)
src/app/(received)/received/interesting/page.tsx (4)

3-3: 불필요한 주석 제거 제안
import React, { useEffect } from 'react'; 구문은 정상적이지만, 만약 더 이상 필요치 않은 주석이나 추가 설명이 있다면 제거를 고려해 주세요. 간결한 코드 유지에 도움이 됩니다.


5-6: 쿼리 훅과 스토어 훅 분리의도 확인
useReceivedCardsQueryuseReceivedCardsStore를 함께 사용해 명함 데이터를 가져오고, 상태를 저장하는 것으로 보입니다. 현재 구조는 문제 없으나, 동일 데이터 흐름이 중복되지 않도록 주의해 주세요.


15-16: 서버 데이터와 스토어 데이터의 일관성
서버에서 받은 카드 리스트(serverReceivedCards)와 isLoading을 함께 받아 사용하는 로직이 적절합니다. 단, 뒤이어 useEffect에서 조건을 검사하므로, isLoadingfalse 때 자동으로 스토어에 데이터를 추가하는 흐름도 고려해볼 수 있습니다.


25-25: Appbar 컴포넌트에 대한 prop 확인
page="mypage" / title="흥미로운 명함" / onLeftClick={handleBack} 조합이 정상 동작하는지 확인 바랍니다. 특히 onLeftClick 콜백이 올바른 라우팅 혹은 화면 전환을 처리할 수 있는지 체크해 주세요.

src/features/received/ui/receivedCardView.tsx (14)

17-17: 폴더 리스트 UI 컴포넌트 네이밍
FoldersList는 직관적인 이름입니다. 다만, 사용처가 늘어날 경우 재사용성이 확보되도록 최대한 범용적으로 작성하는 것도 좋습니다.


19-19: ReceivedCardList 컴포넌트 의존성
ReceivedCardListselectedFolderId만 받아서 렌더링하는 구조가 명확하고 단순합니다. props 검증(PropTypes or TS interface)도 적절히 되어 있는지 점검해 주세요.


26-29: 상태 관리 네이밍
isUpdate, isAdd 등 boolean 변수가 직관적이어서 좋습니다. 향후 상태가 복잡해지면 enum이나 별도 상태 정의가 필요할 수 있습니다.


35-35: useFolderStore 디스트럭처링
folders, addFolder, updateFolder, deleteFolder를 한번에 가져오고 있습니다. UI 상에서 잘 구분되어 사용되는지 확인 부탁드립니다.


41-43: 폴더 선택 핸들러
handleFolderSelect에서 setSelectedFolderId만 호출하도록 구성하셨습니다. 이 로직이 외부에서 정상적으로 useQuery 등과 연동되는지 다시 한번 확인해 주세요.


45-47: 폴더 추가 핸들러
isAddtrue로 설정하는 간단한 로직이지만, Modal이나 렌더링 분기 처리가 제대로 동작하는지 한 번 더 살펴보면 좋겠습니다.


48-51: 폴더 수정 핸들러
폴더 이름을 받아 folderName을 설정하고 isUpdate = true로 전환하는 구조가 깔끔합니다. 수정 로직이 분리되어 있어 유지보수에 용이합니다.


52-57: 폴더 삭제 로직 및 UI 연동
서버 삭제(serverDeleteFolder) 후 로컬 스토어(deleteFolder)까지 업데이트하여 상태를 동기화하는 흐름이 올바릅니다. 성공 후 toast.success가 뜨는 시점도 적절합니다.


59-71: 폴더 수정 시 Enter 키 이벤트 처리
handleUpdateKeyDown 내에서 Enter 이벤트 시 로컬과 서버 모두 업데이트한 뒤, 모달을 닫고 Toast 메시지를 띄웁니다. 사용자가 폴더 이름을 수정한 뒤 바로 Enter를 누르기만 하면 되므로 UX가 좋을 것으로 보입니다.


72-82: 폴더 추가 시 Enter 키 이벤트 처리
수정 로직과 유사하게 handleAddKeyDown은 신규 폴더 추가 과정을 잘 처리합니다. Enter 키 하나로 손쉽게 반영되는 점이 장점입니다.


84-89: 모달이 닫힐 때 상태 초기화
isModalOpen이 닫히면 isUpdate, isAdd 상태를 모두 false로 초기화하는 방식이 간단하고 확실합니다. 모달 밖 다른 로직에 영향이 없는지 한 번만 더 확인해 주세요.


104-104: FoldersList 컴포넌트 렌더링
<FoldersList handleFolderSelect={handleFolderSelect} />를 통해 폴더 리스트를 보여주고 선택 시 상위 컴포넌트의 상태를 업데이트하는 구조가 명확합니다.


105-105: 스크롤바 숨김 클래스 재확인
scrollbar-hide 클래스가 잘 동작하는지, 모바일 환경에서 정상 동작하는지 한 번 더 점검해 보세요.


115-115: ReceivedCardList 연동
<ReceivedCardList selectedFolderId={selectedFolderId} />를 통해 폴더 선택 상태가 명함 목록에 정상 전달됩니다. 프롭 타입 정의를 통해 selectedFolderId가 null일 때 전체 목록을 보여주는지 재확인 바랍니다.

src/app/(received)/received/page.tsx (6)

19-20: 폴더 선택 State
selectedFolderId에 대해 null일 때 전체 폴더, 숫자일 때 특정 폴더 식으로 로직이 짜여 있으면 좋습니다. 이미 적용되어 있다면 문제 없습니다.


21-21: useFoldersQuery와 로딩 상태
isLoading: isFoldersLoading라는 별칭 사용이 가독성에 좋습니다.


25-26: 스토어 연결
setFolders, setReceivedCards를 통한 로컬 스토어 동기화가 올바르게 동작하는지, 해당 스토어가 어디에서 사용되고 있는지 점검 부탁드립니다.


28-29: 폴더 데이터 세팅 useEffect
isFoldersLoading === false 시점에 serverFolderssetFolders하는 로직이 적절합니다. 다만, 폴더 데이터가 변경될 때마다 갱신이 이뤄질 수 있도록 의존성 배열 역시 적합하게 구성돼 있습니다.


34-34: 스토어 업데이트 의존성
setReceivedCards가 제대로 동작하는지, 다른 컴포넌트에서 해당 스토어 데이터가 업데이트되는 타이밍에 맞춰 갱신되는지 검증해 주세요.


43-43: ReceivedCardView 연결
<ReceivedCardView selectedFolderId={selectedFolderId} setSelectedFolderId={setSelectedFolderId} />를 통해 선택 폴더 상태를 전달하는 방식이 명확합니다. 문제 없어 보입니다.

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

♻️ Duplicate comments (2)
src/features/received/ui/choose/chooseReceivedCardView.tsx (2)

36-44: 🛠️ Refactor suggestion

삭제 및 폴더 이동 함수 개선 제안

현재 handleDelete 함수는 서버 응답을 기다리지 않고 즉시 UI를 업데이트하고 있습니다. 이는 서버 요청이 실패할 경우 UI와 서버 상태가 불일치하게 될 위험이 있습니다. useDeleteReceivedCards와 useMoveCardToFolder의 onSuccess 콜백을 활용하는 것이 더 안전합니다.

const handleDelete = () => {
-  deleteCards(selectedCardIds);
-  deleteServerCards({ cardIds: selectedCardIds });
+  deleteServerCards({ 
+    cardIds: selectedCardIds,
+    onSuccess: () => {
+      deleteCards(selectedCardIds);
+      setSelectedCardIds([]);
+    }
+  });
};

const handleMoveToFolder = (folderId: number) => {
-  moveToFolder({ folderId: folderId, cardIds: selectedCardIds });
+  moveToFolder({ 
+    folderId: folderId, 
+    cardIds: selectedCardIds,
+    onSuccess: () => {
+      setSelectedCardIds([]);
+      closeSettingModal();
+    } 
+  });
-  closeSettingModal();
};

46-50: 🛠️ Refactor suggestion

useEffect의 의존성 배열 및 조건부 로직 개선

현재 useEffect는 isFetching이 변경될 때마다 실행되고, isFetching이 false일 때 카드를 설정합니다. 하지만 이 접근 방식은 다음과 같은 문제가 있을 수 있습니다:

  1. isFetching은 요청 시작과 완료 시점 모두에서 false에서 true로, true에서 false로 변경될 수 있어 불필요한 렌더링이 발생할 수 있습니다.
  2. 데이터가 undefined일 때도 setReceivedCards가 호출될 수 있습니다.
useEffect(() => {
-  if (!isFetching) {
+  if (!isFetching && cards.length > 0) {
    setReceivedCards(cards);
  }
-}, [isFetching]);
+}, [isFetching, cards, setReceivedCards]);
🧹 Nitpick comments (9)
src/features/received/model/mutations/useDeleteReceivedCard.ts (1)

19-34: useDeleteReceivedCards 훅의 구현 관련 피드백

훅의 구현은 전반적으로 잘 되어 있으나, 다음 사항을 개선하면 좋겠습니다:

  1. 26-27줄의 console.log는 배포 전에 제거하는 것이 좋습니다.
  2. 특정 폴더의 카드만 삭제했을 때도 모든 폴더의 쿼리를 무효화하게 될 수 있습니다. 이는 불필요한 API 호출을 발생시킬 수 있습니다.
  return useMutation({
    mutationFn: (variables: { cardIds: number[] }) => _deleteReceivedCards(variables.cardIds),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [CARD_QUERY_KEY, folderId] });
-     console.log('받은 명함 삭제 성공');
      toast.success('명함을 삭제했어요');
    },
    onError: (error) => {
      console.error('받은 명함 삭제 실패:', error);
      toast.error('삭제에 실패했어요. 다시 시도해주세요.');
    },
  });
src/features/received/model/mutations/useDeleteFolder.ts (1)

19-34: useDeleteFolder 훅의 구현 관련 피드백

폴더 삭제 훅의 구현은 전반적으로 잘 되어 있으나, 다음 사항을 개선하면 좋겠습니다:

  1. 25-26줄의 console.log는 배포 전에 제거하는 것이 좋습니다.
  2. 쿼리 무효화 로직이 onSuccess 부분에서 toast 메시지보다 아래에 위치하고 있습니다. 일관성을 위해 다른 훅과 동일한 순서로 배치하는 것이 좋습니다.
  return useMutation({
    mutationFn: (variables: { folderId: number }) => _deleteFolder(variables.folderId),
    onSuccess: () => {
-     console.log('폴더 삭제 성공');
+     queryClient.invalidateQueries({ queryKey: [FOLDER_QUERY_KEY] });
      toast.success('폴더가 삭제되었어요.');
-     queryClient.invalidateQueries({ queryKey: [FOLDER_QUERY_KEY] });
    },
    onError: (error) => {
      console.error('폴더 삭제 실패:', error);
      toast.error('폴더 삭제에 실패했어요. 다시 시도해주세요.');
    },
  });
src/features/received/ui/choose/chooseReceivedCardView.tsx (4)

30-34: toggleChecked 함수 내 console.log 제거 필요

toggleChecked 함수 내에 디버깅용 console.log가 남아있습니다. 배포 전에 제거해주세요.

const toggleChecked = (id: number) => {
  setSelectedCardIds((prev) => (prev.includes(id) ? prev.filter((cardId) => cardId !== id) : [...prev, id]));
- console.log(selectedCardIds);
};

52-52: 로딩 상태 UI 개선 필요

현재 로딩 상태 표시는 임시 구현이라고 주석에 명시되어 있습니다. 실제 프로덕션에서는 Skeleton UI나 스피너 같은 보다 사용자 친화적인 로딩 인디케이터를 구현하는 것이 좋습니다.

프로젝트에 사용 중인 디자인 시스템에 맞는 로딩 컴포넌트를 활용하는 것을 권장합니다.


81-94: 카드 목록 렌더링 최적화 제안

카드 목록을 렌더링할 때 key로 index를 사용하고 있습니다. 목록의 순서가 변경되거나 항목이 추가/제거될 때 React의 재조정 알고리즘에 문제가 발생할 수 있습니다. 또한 동일한 key를 ReceivedCard 컴포넌트에도 사용하고 있습니다.

{receivedCards.map((card, index) => {
  return (
-   <div key={index} className="flex max-w-full items-center gap-4">
+   <div key={card.id} className="flex max-w-full items-center gap-4">
      <ReceivedCheckbox
        checked={selectedCardIds.includes(card.id)}
        onCheckedChange={() => toggleChecked(card.id)}
      />

      <div className="min-w-0 flex-1">
-       <ReceivedCard key={index} cardData={card} />
+       <ReceivedCard cardData={card} />
      </div>
    </div>
  );
})}

95-105: 바텀 모달 및 폴더 목록 관련 개선점

폴더 목록을 표시하는 바텀 모달에서 다음과 같은 개선 사항을 고려해볼 수 있습니다:

  1. 폴더 항목에 폴더 아이콘 추가하여 시각적 구분 강화
  2. 폴더 내 카드 수 표시하여 사용자에게 추가 정보 제공
  3. 폴더가 없을 때 안내 메시지 표시

폴더 목록이 비어 있을 때 안내 메시지를 추가하는 예시:

<BottomModal isModalOpen={isSettingModalOpen} closeModal={closeSettingModal}>
  <BottomModalTitle>폴더 설정</BottomModalTitle>
+ {folders.length === 0 && (
+   <div className="flex justify-center items-center p-4 text-gray-400">
+     생성된 폴더가 없습니다. 폴더를 추가해보세요.
+   </div>
+ )}
  {folders.map((folder, index) => {
    return (
      <BottomMenuItem key={index} onClick={() => handleMoveToFolder(folder.id)}>
        {folder.name}
      </BottomMenuItem>
    );
  })}
</BottomModal>
src/app/(received)/received/page.tsx (1)

28-34: useEffect 사용 최적화 필요

서버 데이터를 로컬 스토어에 동기화하는 useEffect 훅들이 구현되어 있습니다. 이전 리뷰 코멘트에서도 언급되었듯이, useEffect 로직을 최적화할 수 있습니다.

  1. 두 개의 useEffect를 하나로 합칠 수 있습니다.
  2. 조건부 로직에 데이터 존재 여부도 확인하는 것이 좋습니다.
- useEffect(() => {
-   if (!isFoldersLoading) setFolders(serverFolders);
- }, [isFoldersLoading, serverFolders, setFolders]);
- 
- useEffect(() => {
-   if (!isCardsLoading) setReceivedCards(serverReceivedCards);
- }, [isCardsLoading, serverReceivedCards, setReceivedCards]);
+ useEffect(() => {
+   if (!isFoldersLoading && serverFolders.length > 0) {
+     setFolders(serverFolders);
+   }
+   
+   if (!isCardsLoading && serverReceivedCards.length > 0) {
+     setReceivedCards(serverReceivedCards);
+   }
+ }, [
+   isFoldersLoading, isCardsLoading, 
+   serverFolders, serverReceivedCards,
+   setFolders, setReceivedCards
+ ]);
src/features/received/ui/receivedCardView.tsx (2)

79-84: 모달이 닫힐 때 newFolderNameupdatedFolderName을 초기화해 주세요.
isModalOpenfalse가 될 때 setIsUpdatesetIsAdd를 리셋하고 있지만, 폴더 이름 관련 다른 상태(newFolderName, updatedFolderName)는 그대로 유지되고 있습니다. 모달을 재오픈할 때 이전 입력값이 남아 사용자 혼동이 발생할 수 있으니 함께 리셋하는 방안을 고려하십시오.


112-123: ‘10/10’ 안내 문구와 실제 길이 제한 검증이 일치해야 합니다.
현재 ‘10/10’라는 안내 문구는 있으나, 폴더명을 10자 초과로 입력했을 때 별도의 처리 없이 서버에 그대로 요청이 전달될 가능성이 있습니다. 사용자의 의도치 않은 입력을 방지하고자, 입력 값의 최대 글자 수를 실제로 제한하거나 초과 시 에러(또는 안내 메시지)를 표시하는 로직을 구현해 주세요.

Also applies to: 124-133

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 140489e and 44f6a3d.

📒 Files selected for processing (12)
  • src/app/(received)/received/interesting/page.tsx (2 hunks)
  • src/app/(received)/received/page.tsx (1 hunks)
  • src/features/received/model/mutations/useCreateFolder.ts (1 hunks)
  • src/features/received/model/mutations/useDeleteFolder.ts (1 hunks)
  • src/features/received/model/mutations/useDeleteReceivedCard.ts (1 hunks)
  • src/features/received/model/mutations/useEditFolder.ts (1 hunks)
  • src/features/received/model/mutations/useMoveCardToFolder.ts (1 hunks)
  • src/features/received/model/queries/useReceivedCardsQuery.ts (1 hunks)
  • src/features/received/ui/choose/chooseReceivedCardView.tsx (1 hunks)
  • src/features/received/ui/foldersList.tsx (1 hunks)
  • src/features/received/ui/receivedCardView.tsx (2 hunks)
  • src/shared/ui/Toast/sonner.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/features/received/model/mutations/useEditFolder.ts
  • src/features/received/model/mutations/useCreateFolder.ts
  • src/shared/ui/Toast/sonner.tsx
  • src/features/received/ui/foldersList.tsx
  • src/features/received/model/queries/useReceivedCardsQuery.ts
  • src/app/(received)/received/interesting/page.tsx
  • src/features/received/model/mutations/useMoveCardToFolder.ts
🧰 Additional context used
🧬 Code Definitions (5)
src/features/received/model/mutations/useDeleteReceivedCard.ts (1)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • CARD_QUERY_KEY (7-7)
src/features/received/model/mutations/useDeleteFolder.ts (1)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • FOLDER_QUERY_KEY (7-7)
src/features/received/ui/choose/chooseReceivedCardView.tsx (4)
src/features/received/model/store/useReceivedCardsStore.ts (1)
  • useReceivedCardsStore (11-18)
src/features/received/model/mutations/useDeleteReceivedCard.ts (1)
  • useDeleteReceivedCards (19-34)
src/features/received/model/mutations/useMoveCardToFolder.ts (1)
  • useMoveCardToFolder (20-33)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (27-34)
src/features/received/ui/receivedCardView.tsx (3)
src/features/received/model/store/useFoldersStore.ts (1)
  • useFolderStore (13-37)
src/features/received/ui/foldersList.tsx (1)
  • FoldersList (14-47)
src/features/received/ui/receivedCardList.tsx (1)
  • ReceivedCardList (12-24)
src/app/(received)/received/page.tsx (5)
src/features/received/model/queries/useReceivedCardsQuery.ts (1)
  • useReceivedCardsQuery (27-34)
src/features/received/model/queries/useFoldersQuery.ts (1)
  • useFoldersQuery (20-27)
src/features/received/model/useModal.ts (1)
  • useModal (3-28)
src/features/received/model/store/useFoldersStore.ts (1)
  • useFolderStore (13-37)
src/features/received/ui/receivedCardView.tsx (1)
  • ReceivedCardView (25-158)
🔇 Additional comments (11)
src/features/received/model/mutations/useDeleteReceivedCard.ts (2)

1-7: API 통신을 위한 적절한 모듈을 임포트하였습니다.

React Query를 활용한 mutation과 toast 알림을 위한 필요 모듈들을 잘 가져왔습니다.


9-17: 받은 명함 삭제 로직이 적절하게 구현되었습니다.

명함 ID 배열을 받아 API를 호출하는 private 함수가 잘 구현되어 있습니다. 에러 처리와
로깅도 적절합니다.

src/features/received/model/mutations/useDeleteFolder.ts (2)

1-7: API 통신을 위한 적절한 모듈을 임포트하였습니다.

React Query를 활용한 mutation과 toast 알림을 위한 필요 모듈들을 잘 가져왔습니다.


9-17: 폴더 삭제 로직이 적절하게 구현되었습니다.

폴더 ID를 받아 API를 호출하는 private 함수가 잘 구현되어 있습니다. 에러 처리와
로깅도 적절합니다.

src/features/received/ui/choose/chooseReceivedCardView.tsx (2)

1-17: 적절한 모듈 임포트 및 컴포넌트 구성

필요한 모듈과 컴포넌트들을 잘 임포트하였습니다. 명함 관리에 필요한 mutation과 query 훅도 잘 구성되어 있습니다.


19-29: 컴포넌트 상태 관리가 잘 구현되어 있습니다.

필요한 상태와 스토어 훅을 적절히 사용하여 컴포넌트를 구성했습니다.
isAnyChecked 변수를 통해 선택된 카드가 있는지 확인하는 로직도 좋습니다.

src/app/(received)/received/page.tsx (4)

3-9: 적절한 모듈 임포트 및 구조화

필요한 모듈들을 잘 임포트하였고, 특히 폴더와 카드 관련 쿼리 및 스토어 훅들이 적절히 구성되어 있습니다.


19-21: 상태 관리 및 쿼리 훅 사용이 적절합니다

선택된 폴더 ID를 상태로 관리하고 이를 쿼리 훅에 전달하는 방식이 잘 구현되어 있습니다. React Query의 장점을 잘 활용한 구조입니다.


41-44: 컴포넌트 속성 전달이 잘 구현되어 있습니다

Appbar와 ReceivedCardView 컴포넌트에 필요한 속성들이 잘 전달되고 있습니다. 특히 selectedFolderId를 null과 함께 안전하게 처리한 점이 좋습니다.


45-54: 모달 및 라우팅 처리가 적절합니다

명함 선택 모달과 라우팅 처리가 잘 구현되어 있습니다. 모달을 닫고 라우팅을 처리하는 순서도 적절합니다.

src/features/received/ui/receivedCardView.tsx (1)

134-154: 폴더 리스트 처리 로직이 직관적으로 잘 구성되어 있습니다.
각 폴더별로 수정·삭제 UI를 직관적으로 구성했고, ‘추가하기’ 버튼으로 새로운 폴더를 등록할 수 있어 기능적으로 문제가 없어 보입니다.

Comment on lines +57 to +77
const handleUpdateKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const index = folders.findIndex((folder) => folder.name === folderName);

if (e.nativeEvent.isComposing) return;
if (e.key == 'Enter') {
e.preventDefault();
const folderId = folders[index].id;
updateFolder(folderId, updatedFolderName);
serverEditFolder({ folderId, name: updatedFolderName });
closeModal();
}
};
const handleAddKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return;
if (e.key == 'Enter') {
e.preventDefault();
serverCreateFolder(newFolderName);
addFolder(newFolderName);
closeModal();
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

폴더 수정/추가 시 에러 처리와 입력 검증이 필요합니다.
handleUpdateKeyDownhandleAddKeyDown 모두 성공/실패 여부와 상관없이 로컬 스토어를 먼저 변경하고 모달을 닫고 있습니다. 서버 호출이 실패할 경우를 대비해, 에러 처리를 고려해야 합니다. 또한 폴더명이 빈 문자열이거나 특정 길이를 초과할 때를 대비한 유효성 검사 로직을 추가하면 사용자의 실수를 방지할 수 있습니다.

Comment on lines +51 to +55
const handleDelete = (id: number) => {
serverDeleteFolder({ folderId: id });
deleteFolder(id);
closeModal();
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

서버 호출 결과에 대한 에러 처리가 필요해 보입니다.
로컬 스토어(deleteFolder)가 서버 응답 결과와 무관하게 먼저 실행되어, 서버 삭제가 실패했을 경우 폴더 정보가 일관성 없이 삭제될 위험이 있습니다. 다음과 같이 React Query의 onSuccess, onError 콜백을 통해 정확히 처리하는 방안을 고려해 주세요.

- const handleDelete = (id: number) => {
-   serverDeleteFolder({ folderId: id });
-   deleteFolder(id);
-   closeModal();
- };
+ const handleDelete = (id: number) => {
+   serverDeleteFolder(
+     { folderId: id },
+     {
+       onSuccess: () => {
+         deleteFolder(id);
+         closeModal();
+       },
+       onError: () => {
+         // 에러 처리 로직 추가 예시
+         console.error('서버 폴더 삭제 중 오류가 발생했습니다');
+       },
+     }
+   );
+ };

@youngju6143 youngju6143 merged commit b48fc49 into develop Apr 1, 2025
2 of 3 checks passed
@youngju6143 youngju6143 deleted the feat/#134-received-card branch April 1, 2025 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🚀 feature 새로운 기능 개발
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feat] 받은 명함 페이지 - API 연결 로직, 바텀 시트 추가 구현
4 participants