Skip to content

Conversation

@LeeCh0129
Copy link

@LeeCh0129 LeeCh0129 commented Jun 19, 2025

📌 변경 사항 개요

내 대시보드 페이지 구현(내 대시보드 목록, 초대받은 대시보드, 검색, 페이지네이션, 무한스크롤)

✨ 요약

  • 내 대시보드 목록 조회 (3x2 그리드, 페이지네이션)
  • 초대받은 대시보드 테이블 (무한스크롤, 검색, 수락/거절)
  • 토스트 알림
  • 기존의 모달을 공통 모달로 적용

📝 상세 내용

내 대시보드 그리드 (MyDashboardGrid.tsx)

  • 3x2 그리드 레이아웃: 새로운 대시보드 추가 카드 + 대시보드 카드 5개 배치
  • 페이지네이션 "X 페이지 중 Y" 형태, 이전/다음 버튼 포함
  • 상태별 UI: 로딩 스켈레톤,에러 메시지, 빈 상태 처리 등
  • 공통 모달 연동: 기존의 새 대시보드 생성 버튼을 GlobalModalRenderer와 연동

초대받은 대시보드 테이블 (InvitedDashboardTable.tsx)

  • 무한스크롤: 80% 스크롤 지점에서 자동 로딩 (6개씩) => 6개씩 한 이유는 피그마 디자인 상으로는 6개
  • 실시간 검색: 대시보드명 + 초대자명 동시 검색
  • 수락/거절: 토스트 알림, 처리 중 상태, 중복 클릭 방지(isProcessing 상태 + disabled)

병합 작업

  • 기존의 대시보드 생성 모달을 공통 컴포넌트로 통합

🔗 관련 이슈

#86

🖼️ 스크린샷

image image image image image image

✅ 체크리스트

  • 브랜치 네이밍 컨벤션을 준수했습니다
  • 커밋 컨벤션을 준수했습니다
  • 코드가 프로젝트의 스타일 가이드라인을 준수합니다

💡 참고 사항

API 엔드포인트:

  • GET /{teamId}/dashboards (내 대시보드 목록)
  • GET /{teamId}/invitations (초대받은 대시보드 목록)
  • PUT /{teamId}/invitations/{invitationId} (초대 수락/거절)

추후 개선 예정:

  • 내 대시보드 반응형 작업
  • 사이드바 캐시 무효화 이슈
  • 사이드바 스켈레톤 UI

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • 대시보드 목록 및 초대받은 대시보드 목록을 한 화면에서 확인할 수 있는 "내 대시보드" 페이지가 추가되었습니다.
    • 대시보드 초대 목록에서 초대를 수락하거나 거절할 수 있습니다.
    • 대시보드 검색 및 무한 스크롤 기능이 초대받은 대시보드 목록에 적용되었습니다.
    • 새 대시보드 추가 버튼이 제공되어 손쉽게 대시보드를 생성할 수 있습니다.
    • 각 대시보드는 카드 형태로 표시되며, 페이지네이션을 통해 여러 페이지를 탐색할 수 있습니다.
  • Style

    • 텍스트 및 테두리 색상 관련 유틸리티 클래스가 추가되어 색상 표현이 다양해졌습니다.

@LeeCh0129 LeeCh0129 added this to the 2차 구현 기간 milestone Jun 19, 2025
@LeeCh0129 LeeCh0129 self-assigned this Jun 19, 2025
@LeeCh0129 LeeCh0129 added the ✨Feat 기능 개발 label Jun 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jun 19, 2025

"""

Walkthrough

이 변경사항은 "내 대시보드" 페이지를 신설하며, 대시보드 목록, 초대받은 대시보드 목록, 초대 수락/거절 기능, 무한 스크롤, 검색, 페이지네이션 등 대시보드 관리와 관련된 주요 기능을 위한 React 컴포넌트, API 모듈, 타입, 커스텀 훅, CSS 유틸리티 클래스를 추가합니다.

Changes

파일/경로 변경 요약
src/app/globals.css 텍스트 및 테두리 색상 관련 3가지 Tailwind 유틸리티 클래스 추가 및 중복 .Text-blue 클래스 제거
src/app/mydashboard/api/dashboardApi.ts 대시보드 및 초대 관련 API 함수 3종(getMyDashboards, getInvitedDashboards, respondToInvitation) 신설
src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardRow.tsx 초대받은 대시보드 한 줄 렌더링 및 수락/거절 처리 컴포넌트 신설
src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardTable.tsx 초대받은 대시보드 목록, 무한스크롤, 검색, 상태별 UI 처리 컴포넌트 신설
src/app/mydashboard/components/InvitedDashboardTable/SearchInput.tsx 검색 입력 필드 컴포넌트 신설
src/app/mydashboard/components/MyDashboardGrid/AddDashboardCard.tsx 대시보드 추가 버튼 컴포넌트 신설
src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx 내 대시보드 카드 컴포넌트 신설
src/app/mydashboard/components/MyDashboardGrid/MyDashboardGrid.tsx 내 대시보드 목록, 페이지네이션, 에러/로딩/성공 상태 처리 컴포넌트 신설
src/app/mydashboard/hooks/useInfiniteScroll.ts 무한 스크롤 커스텀 훅 신설
src/app/mydashboard/hooks/useMyDashboards.ts 대시보드 및 초대 관련 React Query 훅(useMyDashboards, useInvitedDashboards, useRespondToInvitation) 신설
src/app/mydashboard/page.tsx 전체 "내 대시보드" 페이지 레이아웃 및 섹션 컴포넌트 신설
src/app/shared/types/dashboard.ts 초대, 초대 응답, 모달 상태 등 대시보드 관련 타입 3종 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MyDashboardPage
    participant MyDashboardGrid
    participant InvitedDashboardTable
    participant API

    User->>MyDashboardPage: 페이지 진입
    MyDashboardPage->>MyDashboardGrid: 내 대시보드 목록 요청
    MyDashboardPage->>InvitedDashboardTable: 초대받은 대시보드 목록 요청
    MyDashboardGrid->>API: getMyDashboards(page, size)
    API-->>MyDashboardGrid: DashboardListResponse
    InvitedDashboardTable->>API: getInvitedDashboards(size, cursorId)
    API-->>InvitedDashboardTable: InvitationListResponse

    User->>InvitedDashboardTable: 초대 수락/거절 클릭
    InvitedDashboardTable->>API: respondToInvitation(invitationId, accept)
    API-->>InvitedDashboardTable: (성공/실패 응답)
    InvitedDashboardTable->>API: (성공 시) 초대/내 대시보드 목록 재요청
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers

  • dkslel1225
  • Insung-Jo

Poem

🐇
대시보드 초대장이 폴짝폴짝,
무한 스크롤에 토끼 귀가 깜빡!
새 카드, 새 친구, 초대 수락,
버튼을 누르면 희망이 반짝.
코드를 타고 달려온 봄,
내 대시보드엔 행운이 솜!
🥕✨
"""

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

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.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-20T14_37_51_814Z-debug-0.log


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb7003f and 577c26e.

📒 Files selected for processing (1)
  • src/app/mydashboard/hooks/useMyDashboards.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/mydashboard/hooks/useMyDashboards.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: eslint-check
✨ 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate sequence diagram to generate a sequence diagram of the changes in this 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.

@LeeCh0129 LeeCh0129 linked an issue Jun 19, 2025 that may be closed by this pull request
9 tasks
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 (7)
src/app/mydashboard/components/MyDashboardGrid/AddDashboardCard.tsx (1)

15-30: 컴포넌트 구현 승인 및 접근성 개선 제안

새로운 대시보드 추가 카드가 잘 구현되었습니다. 스타일링과 상태 관리가 적절합니다.

접근성 향상을 위해 aria-label 속성 추가를 고려해보세요:

    <button
+     aria-label="새로운 대시보드 만들기"
      onClick={handleClick}
      className="BG-white Border-btn hover:BG-gray group flex h-70 w-332 items-center justify-center gap-12 rounded-8 border p-20 transition-all duration-200 hover:border-gray-300"
    >
src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx (1)

20-63: 카드 컴포넌트 구현 승인 및 접근성 개선 제안

대시보드 카드의 UI/UX가 잘 구현되었습니다. 컬러 도트, 제목 truncation, 조건부 왕관 아이콘 등이 적절히 처리되었습니다.

키보드 접근성 향상을 위해 다음 개선사항을 고려해보세요:

    <div
+     role="button"
+     tabIndex={0}
+     onKeyDown={(e) => {
+       if (e.key === 'Enter' || e.key === ' ') {
+         e.preventDefault()
+         handleClick()
+       }
+     }}
      onClick={handleClick}
      className="BG-white Border-btn hover:BG-gray group h-70 w-332 cursor-pointer rounded-8 border p-20 transition-all duration-200 hover:border-gray-300"
    >
src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardRow.tsx (2)

70-83: 접근성을 위해 버튼에 aria-label을 추가하세요.

버튼의 접근성을 향상시키기 위해 더 구체적인 aria-label을 추가하는 것을 권장합니다.

        <button
          onClick={handleAccept}
          disabled={isProcessing}
+         aria-label={`${invitation.dashboard.title || '제목 없음'} 대시보드 초대 수락`}
          className="BG-blue flex h-32 w-70 items-center justify-center rounded-8 text-14 font-medium text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {isProcessing ? '처리 중' : '수락'}
        </button>
        <button
          onClick={handleReject}
          disabled={isProcessing}
+         aria-label={`${invitation.dashboard.title || '제목 없음'} 대시보드 초대 거절`}
          className="BG-white Border-blue Text-blue flex h-32 w-70 items-center justify-center rounded-8 border text-14 font-medium transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {isProcessing ? '처리 중' : '거절'}
        </button>

31-32: 에러 로깅에서 민감한 정보 노출 방지를 고려하세요.

콘솔에 전체 에러 객체를 로깅하면 민감한 정보가 노출될 수 있습니다. 프로덕션 환경에서는 에러 메시지만 로깅하는 것을 고려해보세요.

    } catch (error) {
-     console.error('초대 수락 실패:', error)
+     console.error('초대 수락 실패:', error instanceof Error ? error.message : '알 수 없는 오류')
      showError('초대 수락 중 오류가 발생했습니다.')
    }
    } catch (error) {
-     console.error('초대 거절 실패:', error)
+     console.error('초대 거절 실패:', error instanceof Error ? error.message : '알 수 없는 오류')
      showError('초대 거절 중 오류가 발생했습니다.')
    }

Also applies to: 49-50

src/app/mydashboard/api/dashboardApi.ts (3)

14-26: 매개변수 검증을 추가하세요.

페이지 번호와 크기에 대한 유효성 검사를 추가하여 API 호출의 안정성을 향상시킬 수 있습니다.

export const getMyDashboards = async (
  page: number = 1,
  size: number = 5,
): Promise<DashboardListResponse> => {
+  if (page < 1) {
+    throw new Error('페이지 번호는 1 이상이어야 합니다.')
+  }
+  if (size < 1 || size > 100) {
+    throw new Error('페이지 크기는 1-100 사이여야 합니다.')
+  }
+
  const params = new URLSearchParams({
    navigationMethod: 'pagination',
    page: page.toString(),
    size: size.toString(),
  })

  const response = await authHttpClient.get(`/${TEAM_ID}/dashboards?${params}`)
  return response.data
}

33-48: 매개변수 검증을 추가하세요.

초대받은 대시보드 조회 함수에도 크기 매개변수 검증을 추가하는 것이 좋습니다.

export const getInvitedDashboards = async (
  size: number = 10,
  cursorId?: number,
): Promise<InvitationListResponse> => {
+  if (size < 1 || size > 100) {
+    throw new Error('페이지 크기는 1-100 사이여야 합니다.')
+  }
+  if (cursorId !== undefined && cursorId < 0) {
+    throw new Error('커서 ID는 0 이상이어야 합니다.')
+  }
+
  const params = new URLSearchParams({
    navigationMethod: 'infiniteScroll',
    size: size.toString(),
  })

  if (cursorId) {
    params.append('cursorId', cursorId.toString())
  }

  const response = await authHttpClient.get(`/${TEAM_ID}/invitations?${params}`)
  return response.data
}

55-62: 매개변수 검증을 추가하세요.

초대 응답 함수에도 ID 검증을 추가하는 것이 좋습니다.

export const respondToInvitation = async (
  invitationId: number,
  accept: boolean,
): Promise<void> => {
+  if (invitationId < 1) {
+    throw new Error('초대 ID는 1 이상이어야 합니다.')
+  }
+
  await authHttpClient.put(`/${TEAM_ID}/invitations/${invitationId}`, {
    inviteAccepted: accept,
  })
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05d9470 and 612df5a.

⛔ Files ignored due to path filters (5)
  • public/images/Rectangle.svg is excluded by !**/*.svg
  • public/images/arrow.svg is excluded by !**/*.svg
  • public/images/chip.svg is excluded by !**/*.svg
  • public/images/search.svg is excluded by !**/*.svg
  • public/images/unsubscribe.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • src/app/globals.css (3 hunks)
  • src/app/mydashboard/api/dashboardApi.ts (1 hunks)
  • src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardRow.tsx (1 hunks)
  • src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardTable.tsx (1 hunks)
  • src/app/mydashboard/components/InvitedDashboardTable/SearchInput.tsx (1 hunks)
  • src/app/mydashboard/components/MyDashboardGrid/AddDashboardCard.tsx (1 hunks)
  • src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx (1 hunks)
  • src/app/mydashboard/components/MyDashboardGrid/MyDashboardGrid.tsx (1 hunks)
  • src/app/mydashboard/hooks/useInfiniteScroll.ts (1 hunks)
  • src/app/mydashboard/hooks/useMyDashboards.ts (1 hunks)
  • src/app/mydashboard/page.tsx (1 hunks)
  • src/app/shared/types/dashboard.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx (1)
src/app/shared/types/dashboard.ts (1)
  • Dashboard (2-10)
src/app/mydashboard/components/MyDashboardGrid/AddDashboardCard.tsx (1)
src/app/shared/store/useModalStore.ts (1)
  • useModalStore (6-10)
src/app/mydashboard/hooks/useMyDashboards.ts (1)
src/app/mydashboard/api/dashboardApi.ts (3)
  • getMyDashboards (14-26)
  • getInvitedDashboards (33-48)
  • respondToInvitation (55-62)
src/app/mydashboard/components/MyDashboardGrid/MyDashboardGrid.tsx (3)
src/app/mydashboard/hooks/useMyDashboards.ts (1)
  • useMyDashboards (15-24)
src/app/mydashboard/components/MyDashboardGrid/AddDashboardCard.tsx (1)
  • AddDashboardCard (7-31)
src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx (1)
  • MyDashboardCard (12-64)
src/app/mydashboard/api/dashboardApi.ts (1)
src/app/shared/types/dashboard.ts (2)
  • DashboardListResponse (13-17)
  • InvitationListResponse (44-47)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: eslint-check
🔇 Additional comments (21)
src/app/globals.css (2)

42-44: 새로운 유틸리티 클래스 추가 승인

일관된 색상 테마를 위한 유틸리티 클래스가 적절히 추가되었습니다.


75-77: Border 유틸리티 클래스 추가 승인

대시보드 컴포넌트에서 사용할 파란색 테두리 유틸리티 클래스가 다크모드를 고려하여 적절히 구현되었습니다.

src/app/shared/types/dashboard.ts (3)

37-41: 모달 상태 인터페이스 승인

대시보드 생성 모달의 상태 관리를 위한 인터페이스가 명확하게 정의되었습니다. 함수명도 직관적이고 적절합니다.


44-47: 초대 목록 응답 타입 승인

페이지네이션을 위한 cursorId를 nullable로 처리한 것이 적절하며, 초대 배열 구조도 명확합니다.


50-67: 초대 정보 타입 정의 승인

초대자와 피초대자 정보, 수락 상태 등 필요한 모든 필드가 포함되어 있고, inviteAccepted를 nullable로 처리한 것이 적절합니다.

src/app/mydashboard/page.tsx (2)

9-36: 페이지 레이아웃 구현 승인

대시보드 페이지의 구조가 명확하고 컴포넌트 분리가 적절히 이루어졌습니다. 사이드바와 메인 콘텐츠 영역의 레이아웃이 잘 설계되었습니다.

PR 목표에서 언급된 대로 향후 반응형 디자인 개선이 계획되어 있는지 확인해주세요. 현재 고정된 너비값들(ml-300, w-1022)이 모바일에서 문제될 수 있습니다.


25-31: 초대받은 대시보드 섹션 승인

섹션 구조가 명확하고 스타일링이 일관성 있게 적용되었습니다. 헤딩 레벨과 여백 설정도 적절합니다.

src/app/mydashboard/components/MyDashboardGrid/MyDashboardCard.tsx (2)

8-17: 타입 정의 및 핸들러 구현 승인

인터페이스 정의가 명확하고 클릭 핸들러가 적절히 구현되었습니다. Next.js 라우터 사용도 올바릅니다.


38-47: 조건부 왕관 아이콘 렌더링 승인

createdByMe 속성을 기반으로 한 조건부 렌더링이 적절히 구현되었습니다. 이미지 최적화와 접근성도 고려되었습니다.

src/app/mydashboard/components/InvitedDashboardTable/SearchInput.tsx (1)

1-36: 컴포넌트 구현이 깔끔하고 잘 작성되었습니다.

검색 입력 컴포넌트가 올바르게 구현되어 있습니다. 타입 정의, 이벤트 처리, 아이콘 배치 모두 적절합니다.

src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardTable.tsx (3)

26-46: 검색 필터링 로직이 잘 구현되었습니다.

useMemo를 사용한 검색 필터링과 데이터 플래트닝 로직이 성능 최적화를 고려하여 잘 작성되었습니다. 대시보드 제목과 초대자 이름 모두에서 검색이 가능한 것도 좋은 UX입니다.


148-153: 무한 스크롤 로딩 상태 처리가 적절합니다.

검색 중에는 무한 스크롤을 비활성화하는 로직이 올바르게 구현되어 있습니다.


136-138: JSX 내 템플릿 리터럴 사용 시 주의가 필요합니다.

JSX 내에서 백틱을 사용한 템플릿 리터럴이 일부 환경에서 렌더링 문제를 일으킬 수 있습니다.

다음과 같이 수정하는 것을 권장합니다:

-            <p className="Text-gray-light text-16 font-medium">
-              `{searchQuery}`에 대한 검색 결과가 없습니다.
-            </p>
+            <p className="Text-gray-light text-16 font-medium">
+              "{searchQuery}"에 대한 검색 결과가 없습니다.
+            </p>

Likely an incorrect or invalid review comment.

src/app/mydashboard/hooks/useInfiniteScroll.ts (1)

10-41: 무한 스크롤 훅이 효율적으로 구현되었습니다.

스크롤 이벤트 처리, 성능 최적화(passive 리스너, useCallback), 메모리 누수 방지(이벤트 리스너 정리) 모두 적절하게 구현되어 있습니다. 80% 스크롤 지점에서 트리거되는 로직도 사용자 경험에 좋습니다.

src/app/mydashboard/hooks/useMyDashboards.ts (3)

15-24: 대시보드 조회 훅이 적절하게 구성되었습니다.

캐시 설정(staleTime, gcTime), 재시도 로직, 윈도우 포커스 시 재요청 방지 등이 잘 설정되어 있어 불필요한 API 호출을 방지합니다.


27-43: 무한 쿼리 구현이 올바릅니다.

cursor 기반 페이지네이션을 위한 무한 쿼리가 정확하게 구현되어 있습니다. getNextPageParam 로직도 적절합니다.


46-72: 뮤테이션 훅에서 캐시 무효화 로직이 잘 구현되었습니다.

초대 응답 후 관련 쿼리들을 무효화하여 최신 데이터를 유지하는 로직이 적절합니다. 에러 처리도 포함되어 있습니다.

src/app/mydashboard/components/MyDashboardGrid/MyDashboardGrid.tsx (3)

75-114: 페이지네이션 구현이 정확합니다.

페이지네이션 로직, 버튼 비활성화 처리, 페이지 번호 표시 모두 올바르게 구현되어 있습니다. 이전/다음 버튼의 경계 처리도 적절합니다.


22-39: 로딩 상태 처리가 사용자 친화적입니다.

AddDashboardCard는 항상 표시하고 나머지 위치에만 스켈레톤을 보여주는 것이 좋은 UX 패턴입니다.


64-72: 다음과 같이 globs 패턴을 사용해 반응형 Tailwind 유틸리티가 실제로 사용되고 있는지 재확인해보세요.

#!/bin/bash
# tsx/ts 파일에서 sm:, md:, lg:, xl: 접두어가 붙은 Tailwind 유틸리티 검색
rg -A 3 -B 3 '(sm:|md:|lg:|xl:)' -g '*.tsx' -g '*.ts'
src/app/mydashboard/api/dashboardApi.ts (1)

1-62: 전반적으로 잘 구현된 API 모듈입니다.

API 함수들이 명확한 목적을 가지고 있고, TypeScript 타입이 적절히 지정되어 있으며, 일관된 패턴을 따르고 있습니다. URLSearchParams를 사용한 쿼리 파라미터 구성도 적절합니다.

Copy link

@Insung-Jo Insung-Jo left a comment

Choose a reason for hiding this comment

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

수고 많으셨습니다!

Copy link
Contributor

@yuj2n yuj2n left a comment

Choose a reason for hiding this comment

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

찬호님 내 대시보드 페이지 구현 수고 많으셨고 덕분에 무한 스크롤 코드 부분 공부해갈 수 있었습니다~ 스켈레톤 UI도 구현해주셔서 UX 측면에서도 좋을 것 같습니다!!

Comment on lines +66 to +79
{/* 스켈레톤 행들 */}
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className="grid grid-cols-3 items-center gap-20 border-b border-gray-100 py-20 pl-36 pr-32"
>
<div className="h-20 animate-pulse rounded-4 bg-gray-200" />
<div className="h-20 animate-pulse rounded-4 bg-gray-200" />
<div className="h-20 animate-pulse rounded-4 bg-gray-200" />
</div>
))}
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

스켈레톤 UI 구현해주셨군용!! 👍👍

Comment on lines +148 to +153
{/* 무한 스크롤 로딩 인디케이터 - 검색 중에는 표시 안함 */}
{!searchQuery.trim() && isFetchingNextPage && (
<div className="flex justify-center py-20">
<div className="size-32 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" />
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

무한스크롤 구현 방법 얻어갑니당 😮

Comment on lines +1 to +41
import { useCallback, useEffect } from 'react'

/**
* 무한스크롤 훅
*
* @param fetchNextPage - 다음 페이지를 가져오는 함수
* @param hasNextPage - 다음 페이지가 있는지 여부
* @param isFetchingNextPage - 다음 페이지를 가져오는 중인지 여부
*/
export const useInfiniteScroll = (
fetchNextPage: () => void,
hasNextPage: boolean,
isFetchingNextPage: boolean,
) => {
const handleScroll = useCallback(() => {
const scrollTop = window.scrollY // 현재 스크롤 위치
const windowHeight = window.innerHeight // 브라우저 창 높이
const documentHeight = document.documentElement.scrollHeight // 문서 전체 높이

const scrollPercentage = (scrollTop + windowHeight) / documentHeight
const isNearBottom = scrollPercentage >= 0.8 // 80% 스크롤하면 트리거

// 다음 페이지 요청
if (isNearBottom && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
}, [fetchNextPage, hasNextPage, isFetchingNextPage]) // threshold 제거

useEffect(() => {
// 사용자가 스크롤바로 페이지를 스크롤할 떄 발생
window.addEventListener('scroll', handleScroll, { passive: true })
// 사용자가 마우스 휠을 굴릴 때 발생
window.addEventListener('wheel', handleScroll, { passive: true })

// 컴포넌트 언마운트 시 이벤트 리스너 제거
return () => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('wheel', handleScroll)
}
}, [handleScroll]) // 핸들스크롤 함수 변경 시 리스너 재등록
}
Copy link
Contributor

Choose a reason for hiding this comment

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

이 훅은 다른 무한 스크롤을 구현하는 곳에서 재사용해서 쓸 수 있겠네용 🙂

Comment on lines 15 to 72
export const useMyDashboards = (page: number = 1, size = 5) => {
return useQuery({
queryKey: ['myDashboards', page, size],
queryFn: () => getMyDashboards(page, size),
staleTime: 1000 * 60 * 5, // 5분간 fresh 상태 유지
gcTime: 1000 * 60 * 10, // 10분간 캐시 유지
retry: 2,
refetchOnWindowFocus: false, // 창 포커스 시 재요청 방지 -> 불필요한 API 호출 방지
})
}

// 초대받은 대시보드 목록 조회 훅
export const useInvitedDashboards = (size: number = 10) => {
return useInfiniteQuery({
queryKey: ['invitedDashboards', size],
// 페이지별 데이터 조회 함수 (pageParam = cursorId)
queryFn: ({ pageParam }: { pageParam: number | null }) =>
getInvitedDashboards(size, pageParam || undefined),
// 첫 페이지 시작점 (cursorId 없음)
initialPageParam: null,
// 다음 페이지 파라미터 결정 함수
getNextPageParam: (lastPage) => {
// cursorId가 있으면 다음 페이지 존재, 없으면 마지막 페이지
return lastPage.cursorId || null
},
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
})
}

// 초대 응답(수락/거절) 훅
export const useRespondToInvitation = () => {
// 캐시 관리
const queryClient = useQueryClient()

return useMutation({
// 변경 작업을 수행하는 함수
mutationFn: ({
invitationId,
accept,
}: {
invitationId: number
accept: boolean
}) => respondToInvitation(invitationId, accept),

// 성공 시 실행
onSuccess: () => {
// 관련 쿼리들 무효화하여 최신 데이터 다시 fetch
queryClient.invalidateQueries({ queryKey: ['invitedDashboards'] })
// 초대 수락 시 대시보드 목록 업데이트
queryClient.invalidateQueries({ queryKey: ['myDashboards'] })
},
// 실패 시
onError: (error) => {
console.error('초대 응답 실패:', error)
},
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

주석이 자세히 달려있어서 로직 이해가 잘 되네용 🤗

Copy link
Contributor

@dkslel1225 dkslel1225 left a comment

Choose a reason for hiding this comment

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

대시보드 페이지 구현 수고하셨습니다! 무한스크롤 배워갑니당

@LeeCh0129 LeeCh0129 merged commit 92dd511 into develop Jun 20, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨Feat 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feat: 내 대시보드 페이지

5 participants