-
Notifications
You must be signed in to change notification settings - Fork 2
✨ feat: 내 대시보드 페이지 구현 #87
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
Conversation
|
""" Walkthrough이 변경사항은 "내 대시보드" 페이지를 신설하며, 대시보드 목록, 초대받은 대시보드 목록, 초대 수락/거절 기능, 무한 스크롤, 검색, 페이지네이션 등 대시보드 관리와 관련된 주요 기능을 위한 React 컴포넌트, API 모듈, 타입, 커스텀 훅, CSS 유틸리티 클래스를 추가합니다. Changes
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: (성공 시) 초대/내 대시보드 목록 재요청
Possibly related issues
Possibly related PRs
Suggested reviewers
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
npm error Exit handler never called! 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
⛔ Files ignored due to path filters (5)
public/images/Rectangle.svgis excluded by!**/*.svgpublic/images/arrow.svgis excluded by!**/*.svgpublic/images/chip.svgis excluded by!**/*.svgpublic/images/search.svgis excluded by!**/*.svgpublic/images/unsubscribe.svgis 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를 사용한 쿼리 파라미터 구성도 적절합니다.
src/app/mydashboard/components/InvitedDashboardTable/InvitedDashboardRow.tsx
Outdated
Show resolved
Hide resolved
Insung-Jo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고 많으셨습니다!
yuj2n
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
찬호님 내 대시보드 페이지 구현 수고 많으셨고 덕분에 무한 스크롤 코드 부분 공부해갈 수 있었습니다~ 스켈레톤 UI도 구현해주셔서 UX 측면에서도 좋을 것 같습니다!!
| {/* 스켈레톤 행들 */} | ||
| {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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스켈레톤 UI 구현해주셨군용!! 👍👍
| {/* 무한 스크롤 로딩 인디케이터 - 검색 중에는 표시 안함 */} | ||
| {!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> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
무한스크롤 구현 방법 얻어갑니당 😮
| 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]) // 핸들스크롤 함수 변경 시 리스너 재등록 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 훅은 다른 무한 스크롤을 구현하는 곳에서 재사용해서 쓸 수 있겠네용 🙂
| 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) | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석이 자세히 달려있어서 로직 이해가 잘 되네용 🤗
dkslel1225
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
대시보드 페이지 구현 수고하셨습니다! 무한스크롤 배워갑니당
📌 변경 사항 개요
내 대시보드 페이지 구현(내 대시보드 목록, 초대받은 대시보드, 검색, 페이지네이션, 무한스크롤)
✨ 요약
📝 상세 내용
내 대시보드 그리드 (MyDashboardGrid.tsx)
초대받은 대시보드 테이블 (InvitedDashboardTable.tsx)
isProcessing상태 +disabled)병합 작업
🔗 관련 이슈
#86
🖼️ 스크린샷
✅ 체크리스트
💡 참고 사항
API 엔드포인트:
GET /{teamId}/dashboards(내 대시보드 목록)GET /{teamId}/invitations(초대받은 대시보드 목록)PUT /{teamId}/invitations/{invitationId}(초대 수락/거절)추후 개선 예정:
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Style