Skip to content

feat: 모임 목록 페이지 구현 (#39)#56

Merged
mgYang53 merged 4 commits intodevelopfrom
feat/gatherings-list-39
Feb 4, 2026
Merged

feat: 모임 목록 페이지 구현 (#39)#56
mgYang53 merged 4 commits intodevelopfrom
feat/gatherings-list-39

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 4, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

로그인한 사용자가 참여 중인 독서모임 목록을 조회하고 관리할 수 있는 페이지를 구현했습니다.

🔧 변경 사항

  • 모임 목록 조회

    • 전체/즐겨찾기 탭 UI 구현 및 카운트 배지 표시
    • 커서 기반 무한 스크롤 (첫 페이지 12개, 이후 9개씩)
    • 빈 상태(EmptyState) 처리
  • 즐겨찾기 기능

    • 즐겨찾기 토글 기능 (Optimistic Update 적용)
    • 최대 4개 제한 및 에러 핸들링
    • 즐겨찾기 목록 별도 조회
  • 컴포넌트 및 훅

    • GatheringCard: 모임 카드 UI 컴포넌트
    • EmptyState: 빈 상태 UI 컴포넌트
    • useInfiniteScroll: 재사용 가능한 무한 스크롤 커스텀 훅
    • useGatherings: 모임 목록 조회 쿼리 훅
    • useFavoriteGatherings: 즐겨찾기 목록 조회 쿼리 훅
    • useToggleFavorite: 즐겨찾기 토글 뮤테이션 훅
  • API 레이어

    • Gatherings API 엔드포인트 및 타입 정의
    • CursorPage, CursorPageParams 공통 타입 추가

📸 스크린샷 (선택 사항)

image

📄 기타

  • 모임장으로서 만든 모임이 아닌, 참여자로서의 모임은 추후 테스트 예정입니다.

Summary by CodeRabbit

  • 새 기능
    • 모임 목록이 무한 스크롤로 로드됩니다.
    • 즐겨찾기한 모임을 별도 탭에서 확인할 수 있습니다.
    • 모임 카드에서 즐겨찾기를 추가/해제할 수 있으며 즉시 UI에 반영됩니다.
    • 비어 있을 때 상황별 안내 메시지(전체/즐겨찾기)를 표시합니다.
    • 모임 목록 페이지에 탭, 카운트 배지 및 모임 생성 이동 버튼이 추가되었습니다.

- 전체/즐겨찾기 탭 UI 및 카운트 배지 구현
- 커서 기반 무한 스크롤 (첫 페이지 12개, 이후 9개)
- 즐겨찾기 토글 (optimistic update, 최대 4개 제한)
- GatheringCard, EmptyState 컴포넌트 추가
- useInfiniteScroll 재사용 가능 훅 추출
- CursorPaginatedResponse 제네릭 타입 추가
@mgYang53 mgYang53 linked an issue Feb 4, 2026 that may be closed by this pull request
29 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Walkthrough

커서 기반 페이지네이션 타입(CursorPaginatedResponse)과 모임 목록 기능이 추가되었습니다. 관련 엔드포인트, API 래퍼, 훅(useGatherings, useFavoriteGatherings, useToggleFavorite), 컴포넌트(EmptyState, GatheringCard), 무한 스크롤 훅, 그리고 전체/즐겨찾기 탭을 포함한 GatheringListPage가 구현되었습니다.

Changes

Cohort / File(s) Summary
API 타입 확장
src/api/index.ts, src/api/types.ts
CursorPaginatedResponse<T, C> 타입 추가 및 src/api/index.ts에 export 포함(아이템 배열, pageSize, hasNext, nextCursor, optional totalCount).
모임 엔드포인트 및 타입
src/features/gatherings/gatherings.endpoints.ts, src/features/gatherings/gatherings.types.ts
FAVORITES, TOGGLE_FAVORITE 엔드포인트 추가. Gathering 관련 타입들(상태, 역할, 리스트 아이템, 커서, CursorPaginatedResponse 기반 응답, 요청 파라미터) 추가.
모임 API 함수
src/features/gatherings/gatherings.api.ts
getGatherings(), getFavoriteGatherings(), toggleFavorite() API 래퍼 추가 및 관련 타입 임포트 확장.
모임 훅 및 쿼리 키
src/features/gatherings/hooks/*, src/features/gatherings/hooks/index.ts
useGatherings(useInfiniteQuery 사용, 커서 페이징), useFavoriteGatherings, useToggleFavorite(낙관적 업데이트), gatheringQueryKeys.favorites() 추가 및 훅 재-익스포트.
무한 스크롤 공용 훅
src/shared/hooks/useInfiniteScroll.ts, src/shared/hooks/index.ts
IntersectionObserver 기반 재사용 가능한 무한 스크롤 훅 추가(옵션: hasNextPage, isFetchingNextPage, isLoading, enabled, threshold, rootMargin) 및 재-익스포트.
모임 UI 컴포넌트
src/features/gatherings/components/EmptyState.tsx, src/features/gatherings/components/GatheringCard.tsx, src/features/gatherings/components/index.ts
빈 상태 컴포넌트와 모임 카드(즐겨찾기 토글, 키보드 접근성 포함) 추가 및 컴포넌트 재-익스포트.
모임 목록 페이지 통합
src/pages/Gatherings/GatheringListPage.tsx, src/features/gatherings/index.ts
전체/즐겨찾기 탭 UI, 무한 스크롤 연동, 즐겨찾기 토글(낙관적 업데이트 + 롤백), 에러 처리(즐겨찾기 제한 등) 포함 페이지 구현 및 컴포넌트·훅 통합.
린트 규칙 & 미사용 파라미터
eslint.config.js, src/features/meetings/hooks/*
ESLint naming-convention에 leadingUnderscore 허용 추가 및 훅들에서 미사용 파라미터를 _data로 표기하도록 변경.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • [feat] 독서모임 리스트 페이지 구현 #39 — 문제: 커서 기반 모임 리스트 구현 요구. 영향: 본 PR이 CursorPaginatedResponse, 엔드포인트, 훅 등으로 해당 요구를 코드 수준에서 구현함. 대안: 이미 해결된 것으로 보이며 이 PR에 연결 가능.

Possibly related PRs

Suggested reviewers

  • choiyoungae
  • haruyam15
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title clearly and specifically summarizes the main change: implementing a gatherings list page for the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/gatherings-list-39

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mgYang53 mgYang53 self-assigned this Feb 4, 2026
@mgYang53 mgYang53 added feat 새로운 기능 추가 labels Feb 4, 2026
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

🤖 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: 에러 타입 불일치

useFavoriteGatheringsApiError를 사용하지만, 이 훅은 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: 카드 클릭 영역에 키보드 접근성 고려 필요 (참고 사항)

divonClick만 있으면 키보드 사용자가 접근하기 어렵습니다.

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()
+        }
+      }}
     >

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

🤖 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/테스트 환경 가드 추가
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] 독서모임 리스트 페이지 구현

2 participants