Conversation
- 전체/즐겨찾기 탭 UI 및 카운트 배지 구현 - 커서 기반 무한 스크롤 (첫 페이지 12개, 이후 9개) - 즐겨찾기 토글 (optimistic update, 최대 4개 제한) - GatheringCard, EmptyState 컴포넌트 추가 - useInfiniteScroll 재사용 가능 훅 추출 - CursorPaginatedResponse 제네릭 타입 추가
Walkthrough커서 기반 페이지네이션 타입(CursorPaginatedResponse)과 모임 목록 기능이 추가되었습니다. 관련 엔드포인트, API 래퍼, 훅(useGatherings, useFavoriteGatherings, useToggleFavorite), 컴포넌트(EmptyState, GatheringCard), 무한 스크롤 훅, 그리고 전체/즐겨찾기 탭을 포함한 GatheringListPage가 구현되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant GatheringListPage
participant useGatherings
participant TanStackQuery
participant API as API Layer
User->>GatheringListPage: 페이지 진입 / 스크롤
GatheringListPage->>useGatherings: 훅 초기화 호출
useGatherings->>TanStackQuery: useInfiniteQuery 설정 (pageSize, cursor)
TanStackQuery->>API: getGatherings(pageSize, cursor)
API-->>TanStackQuery: CursorPaginatedResponse<GatheringListItem, GatheringCursor>
TanStackQuery-->>useGatherings: 데이터, fetchNextPage, hasNextPage 반환
useGatherings-->>GatheringListPage: 쿼리 상태 제공
User->>GatheringListPage: 즐겨찾기 토글 클릭
GatheringListPage->>useToggleFavorite: mutate(gatheringId)
useToggleFavorite->>TanStackQuery: 관련 쿼리 취소 및 스냅샷 저장
useToggleFavorite->>TanStackQuery: 낙관적 캐시 업데이트 (lists, favorites)
useToggleFavorite->>API: toggleFavorite(gatheringId)
alt 성공
API-->>useToggleFavorite: 성공 응답
useToggleFavorite->>TanStackQuery: favorites 쿼리 무효화
else 실패
API-->>useToggleFavorite: 에러
useToggleFavorite->>TanStackQuery: 스냅샷으로 롤백
useToggleFavorite->>GatheringListPage: 에러 반환
end
sequenceDiagram
participant Sentinel as 센티널 엘리먼트
participant useInfiniteScroll
participant Observer as IntersectionObserver
participant fetchNextPage
useInfiniteScroll->>Observer: 옵저버 생성 (threshold, rootMargin)
useInfiniteScroll->>Observer: 센티널 observe
loop 스크롤 동작
Observer->>useInfiniteScroll: 인터섹션 콜백
useInfiniteScroll->>useInfiniteScroll: 조건 검사 (enabled, hasNextPage, isFetchingNextPage, isLoading)
useInfiniteScroll->>useInfiniteScroll: 스크롤바/스크롤 위치 검사
alt 조건 만족
useInfiniteScroll->>fetchNextPage: fetchNextPage() 호출
else
useInfiniteScroll->>useInfiniteScroll: 호출 스킵
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/shared/hooks/useInfiniteScroll.ts`:
- Line 1: Prettier formatting errors were detected in
src/shared/hooks/useInfiniteScroll.ts; run the formatter (e.g., run "prettier
--write" or your configured npm script) against this file and commit the
resulting changes so imports like the "import { useEffect, useRef } from
'react'" line and the rest of the useInfiniteScroll implementation conform to
project Prettier rules; ensure the file (useInfiniteScroll.ts) is reformatted
and re-checked by CI before pushing.
🧹 Nitpick comments (4)
src/pages/Gatherings/GatheringListPage.tsx (1)
137-154: 즐겨찾기 탭에 로딩 상태 처리 누락
useFavoriteGatherings의 로딩 상태(isLoading)를 처리하지 않아, 데이터 로드 중에 빈 상태(EmptyState)가 잠시 표시될 수 있습니다.♻️ 제안
+ const { data: favoritesData, isLoading: isFavoritesLoading } = useFavoriteGatherings() {activeTab === 'favorites' && ( <> - {favorites.length === 0 ? ( + {isFavoritesLoading ? ( + <div className="flex h-35 items-center justify-center"> + <p className="text-grey-600 typo-subtitle2">로딩 중...</p> + </div> + ) : favorites.length === 0 ? ( <EmptyState type="favorites" /> ) : (src/features/gatherings/hooks/useToggleFavorite.ts (1)
53-68: 즐겨찾기 추가 시 optimistic update 미적용현재 로직은 즐겨찾기 제거만 optimistic하게 처리합니다. 즐겨찾기 추가 시에는
onSettled의 invalidation에 의존하여 UI 업데이트가 지연될 수 있습니다.→ 사용자 경험에 큰 영향은 없으나, 완전한 optimistic update를 원하면 추가 케이스도 처리할 수 있습니다.
♻️ 추가 케이스 처리 예시
queryClient.setQueryData<FavoriteGatheringListResponse>( gatheringQueryKeys.favorites(), (old) => { if (!old) return old const existing = old.gatherings.find((g) => g.gatheringId === gatheringId) if (existing) { // 즐겨찾기에서 제거 return { ...old, gatherings: old.gatherings.filter((g) => g.gatheringId !== gatheringId), } } + // 즐겨찾기에 추가 (lists에서 해당 아이템 찾기) + const listsData = queryClient.getQueryData<{ pages: GatheringListResponse[] }>( + gatheringQueryKeys.lists() + ) + const itemToAdd = listsData?.pages + .flatMap((p) => p.items) + .find((item) => item.gatheringId === gatheringId) + if (itemToAdd) { + return { + ...old, + gatherings: [...old.gatherings, { ...itemToAdd, isFavorite: true }], + } + } return old } )src/features/gatherings/hooks/useGatherings.ts (1)
29-36: 에러 타입 불일치
useFavoriteGatherings는ApiError를 사용하지만, 이 훅은Error를 사용합니다. 일관성을 위해ApiError로 통일하는 것이 좋습니다.♻️ 제안
+ import type { ApiError } from '@/api' + return useInfiniteQuery< GatheringListResponse, - Error, + ApiError, InfiniteData<GatheringListResponse, PageParam>, readonly string[], PageParam >({src/features/gatherings/components/GatheringCard.tsx (1)
32-35: 카드 클릭 영역에 키보드 접근성 고려 필요 (참고 사항)
div에onClick만 있으면 키보드 사용자가 접근하기 어렵습니다.→
role="button",tabIndex={0},onKeyDown추가를 권장합니다.♻️ 접근성 개선 제안
<div className="relative flex h-35 cursor-pointer flex-col justify-between rounded-base border border-grey-300 bg-white p-medium transition-colors hover:border-grey-400" onClick={onClick} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onClick() + } + }} >
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/features/gatherings/components/GatheringCard.tsx`:
- Around line 35-39: The clickable div in GatheringCard.tsx (the element with
className starting "relative flex..." and onClick={onClick}) lacks keyboard
accessibility; update it either by replacing the div with a semantic interactive
element (button or Link) or by adding role="button", tabIndex={0}, and an
onKeyDown handler that invokes the same onClick logic for Enter/Space keys so
keyboard users can activate the card; ensure the onKeyDown handler references
the existing onClick prop and that any focus/outline styling remains visible.
- Around line 30-33: The handler handleFavoriteClick uses React.MouseEvent but
React types aren't imported, causing TypeScript errors; import the MouseEvent
type from 'react' and switch the handler signature to use the imported
MouseEvent (or add an explicit React import) so the line "const
handleFavoriteClick = (e: React.MouseEvent) => { ... }" compiles; keep
references to handleFavoriteClick, onFavoriteToggle and gatheringId unchanged.
🧹 Nitpick comments (1)
src/shared/hooks/useInfiniteScroll.ts (1)
56-78: 비브라우저 환경 가드 추가 권장문제:
window/IntersectionObserver미지원 환경(SSR, 테스트, 구형 브라우저)에서new IntersectionObserver가 런타임 에러를 낼 수 있어요.
영향: 페이지 렌더가 실패하거나 테스트가 깨질 수 있습니다.
대안: 옵저버 생성 전에 환경 체크를 넣어 방어하세요.🛠️ 제안 패치
useEffect(() => { + if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') return // 비활성화 상태이거나 로딩 중이거나 다음 페이지가 없으면 옵저버 설정 안함 if (!observerRef.current || !enabled || isLoading || !hasNextPage) return
- GatheringCard 키보드 접근성 개선 (role, tabIndex, onKeyDown) - TypeScript 타입 에러 수정 (MouseEvent import) - 즐겨찾기 탭 로딩 상태 처리 추가 - useInfiniteScroll SSR/테스트 환경 가드 추가
🚀 풀 리퀘스트 제안
📋 작업 내용
로그인한 사용자가 참여 중인 독서모임 목록을 조회하고 관리할 수 있는 페이지를 구현했습니다.
🔧 변경 사항
모임 목록 조회
즐겨찾기 기능
컴포넌트 및 훅
API 레이어
📸 스크린샷 (선택 사항)
📄 기타
Summary by CodeRabbit