Skip to content

Conversation

@Chasyuss
Copy link
Member

@Chasyuss Chasyuss commented Sep 5, 2025

🚀 풀 리퀘스트 제안

  • 기능 추가
  • 기능 삭제
  • 버그 수정
  • 스타일링
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트
  • 기타

✈️ 관련 이슈

#303

📋 작업 내용

  • 커뮤니티 오른쪽 연결
  • 무한스크롤
  • 내 할일에 추가 연결
  • 내 할일 취소 연결
  • 정렬
  • 스크랩/ 나의 할일 헤더로 분리

📸 스크린샷 (선택 사항)

image

📄 기타

Summary by CodeRabbit

  • New Features
    • 커뮤니티: 무한 스크롤, 레벨 필터, 정렬(최신/인기), 저장/해제 토글 및 성공/오류 토스트 알림 추가
    • 커뮤니티에서 항목 추가/삭제 기능(서버 연동) 및 관련 페이징/갱신 지원
  • Bug Fixes
    • 내비게이션에서 ‘나의 할일’과 ‘스크랩’의 활성화 충돌 해결
  • Refactor
    • 커뮤니티 페이지를 좌/우 컴포넌트로 분리해 UI와 데이터 책임 분리
    • MyTodo 페이지 레이아웃 단순화(직접 Outlet 렌더)
  • Style
    • 스크랩 빈 상태의 최소 높이 제거 및 목록의 로딩/빈/끝 표시 개선

@Chasyuss Chasyuss requested a review from cywin1018 September 5, 2025 10:28
@Chasyuss Chasyuss self-assigned this Sep 5, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

커뮤니티 페이지를 좌/우 컴포넌트로 분리하고 무한 스크롤 기반 조회·추가·삭제 훅을 도입했으며, 헤더에 /mytodo/scrap 항목을 추가하고 /mytodo/list의 활성화 매칭을 정확 경로로 변경했습니다. MyTodo 레이아웃 래퍼 제거와 스크랩 빈 상태 스타일 수정도 포함됩니다.

Changes

Cohort / File(s) Summary
Navigation Update
src/common/Header.tsx
네비게이션에 '스크랩'(/mytodo/scrap) 항목 추가. '나의 할일'(/mytodo/list)의 match 함수 제거로 정확 경로 일치만 활성화되도록 변경.
Community Query & Mutations
src/hook/community/query/useCommunityGetTodo.ts, src/hook/community/useCommunityAddTodoMutation.ts, src/hook/community/useDeleteCommunityTodos.ts
커뮤니티 todo 무한 조회 훅(useCommunityGetTodo) 추가(타입 정의 포함), 추가(POST)·삭제(DELETE) 뮤테이션 훅 추가. 로컬스토리지 토큰 사용, 관련 쿼리 무효화 및 에러 로깅 처리.
Infinite Scroll Utility
src/hook/community/useInfinityScroll.ts
IntersectionObserver 기반 재사용 가능한 무한 스크롤 훅 추가(옵션: enabled, root, rootMargin, threshold, once).
Community UI Refactor
src/pages/community/Community.tsx, src/pages/community/components/CommunityLeftSide.tsx, src/pages/community/components/CommunityRightSide.tsx, src/pages/community/components/CommunityContents.tsx
Community 페이지를 좌/우 컴포넌트로 분리. LeftSide는 직업 선택·HOT 목록 렌더링, RightSide는 useCommunityGetTodo 기반 필터·정렬·무한 스크롤과 로딩/에러 상태 관리, Contents는 props 기반 렌더링(아이템·level·sort) 및 추가/삭제 토글 처리로 변경.
MyTodo & Scrap UI
src/pages/myTodo/MyTodoPage.tsx, src/pages/myTodo/components/scrap/ScrapEmptyState.tsx
MyTodoPage에서 레이아웃 래퍼 제거(직접 Outlet 반환). ScrapEmptyState 루트의 min-h-[500px] 클래스 제거(최소 높이 제약 삭제).

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant UI as CommunityRightSide
  participant Hook as useCommunityGetTodo
  participant API as /v1/community/todos

  User->>UI: 페이지 진입
  UI->>Hook: useInfiniteQuery(initialPage=0)
  Hook->>API: GET ?jobName&level&sort&page=0&size=...
  API-->>Hook: Page 0 (content, last=false)
  Hook-->>UI: data.pages[0]

  User->>UI: 하단 스크롤(IntersectionObserver)
  UI->>Hook: fetchNextPage()
  Hook->>API: GET page=1
  API-->>Hook: Page 1 (last=?)
  Hook-->>UI: 누적 pages 업데이트

  Note over UI,Hook: last=true이면 다음 페이지 요청 중단
Loading
sequenceDiagram
  actor User
  participant CC as CommunityContents
  participant Add as useCommunityAddTodoMutation
  participant Del as useDeleteCommunityTodosMutation
  participant API as /v1/community/todos/{id}

  User->>CC: 할 일 추가/삭제 토글 클릭
  alt 현재 미저장(isSaved=false)
    CC->>Add: mutate({id})
    Add->>API: POST {id} (Bearer token)
    API-->>Add: 200 OK
    Add-->>CC: 성공 콜백 (invalidate 쿼리)
    CC->>CC: 토스트 표시
  else 이미 저장됨(isSaved=true)
    CC->>Del: mutate({id})
    Del->>API: DELETE {id} (Bearer token)
    API-->>Del: 200 OK
    Del-->>CC: 성공 콜백 (invalidate 쿼리)
    CC->>CC: 토스트 표시
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • main 최신화 #312 — Header 네비게이션 항목 및 활성화 매칭 로직을 변경한 PR로 동일 파일 변경과 직접 연관.
  • [✨FEATURE] 커뮤니티 UI  #316 — Community 컴포넌트(Community.tsx/Right/Contents) 관련 리팩터 및 데이터 연동 변경과 중복 영역 존재.
  • [✨FEATURE] 커뮤니티 연결 #328 — 커뮤니티 기능(무한 조회·뮤테이션·컴포넌트 분리)과 매우 유사한 변경을 포함한 PR로 직접 관련.

Suggested labels

✨Feature, 💄Style

Suggested reviewers

  • cywin1018

Poem

깡총, 스크롤 따라 춤을 춰요 🐇
왼쪽은 핫, 오른쪽은 끝없이 늘어나는 할 일들
북마크 톡, 토글 쾅 — 추가하고 빼는 즐거움
토큰 한 줌에 목록이 반짝, 코드도 깡총깡총
축하해요, 새 기능에 귀여운 당근 한 개🥕


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e14c43a and 6e1325c.

📒 Files selected for processing (1)
  • src/hook/community/query/useCommunityGetTodo.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/community-303

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
🪧 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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: 9

🧹 Nitpick comments (28)
src/pages/myTodo/components/scrap/ScrapEmptyState.tsx (2)

10-25: min-height 제거 영향 검토: 상단 고정 헤더와의 겹침/레이아웃 점프 가능성

상단 Header가 fixed(h-20, 80px)인 점을 감안하면, 비어있는 페이지에서 세로 공간이 너무 작아 UI가 상단에 들러붙거나 점프할 수 있습니다. 부모 컨테이너가 높이를 보장하지 않는 경우를 대비해 최소 높이를 다시 지정하는 편이 안전합니다.

아래처럼 헤더 높이를 고려한 최소 높이를 제안합니다.

-  <div className="flex flex-col items-center justify-center space-y-4">
+  <div className="flex min-h-[calc(100vh-80px)] flex-col items-center justify-center space-y-4">

9-25: 반복되는 라벨 계산(삼항연산) 상수 추출로 가독성 개선

'label'을 한 번 계산해 재사용하면 JSX가 더 읽기 쉬워집니다.

-const ScrapEmptyState = ({ type, onNavigate }: Props) => (
-  <div className="flex flex-col items-center justify-center space-y-4">
+const ScrapEmptyState = ({ type, onNavigate }: Props) => {
+  const label = type === 'job' ? '채용' : '학원';
+  return (
+  <div className="flex flex-col items-center justify-center space-y-4">
     <img src={WarningImg} alt="스크랩 비어 있음" className="h-32 w-32" />
     <h2 className="text-gray-900 font-T02-B">
-      스크랩한 {type === 'job' ? '채용' : '학원'} 정보가 아직 없어요!
+      스크랩한 {label} 정보가 아직 없어요!
     </h2>
     <p className="text-center text-gray-500 font-B02-M">
-      관심있는 직업에 필요한 {type === 'job' ? '채용' : '학원'} 정보를
+      관심있는 직업에 필요한 {label} 정보를
       <br />
       차근차근 탐색한 후에 스크랩을 해보세요!
     </p>
     <Button
-      text={`${type === 'job' ? '채용' : '학원'} 정보 둘러보기`}
+      text={`${label} 정보 둘러보기`}
       className="h-[62px] w-[242px] font-T05-SB"
       onClick={onNavigate}
     />
   </div>
-);
+  );
+};
src/pages/myTodo/MyTodoPage.tsx (1)

4-4: SidebarLayout 제거로 인한 헤더(고정) 겹침 여부 점검

Header가 fixed(h-20)라면, Outlet 바로 반환 시 일부 화면에서 상단이 가려질 수 있습니다. 자식 라우트에서 일괄 보정하지 않는다면 여기서 상단 패딩을 주는 방식을 권장합니다.

-  return <Outlet />;
+  return (
+    <div className="pt-20">
+      <Outlet />
+    </div>
+  );
src/common/Header.tsx (1)

32-35: '스크랩' 탭 활성화 조건을 하위 경로까지 확장

현재는 pathname === path(정확히 '/mytodo/scrap')에서만 활성화됩니다. '/mytodo/scrap/...' 하위 경로가 있다면 비활성 상태로 보여질 수 있으니 match 함수를 추가해 startsWith로 통일을 제안합니다. '나의 할일'을 exact로 두려는 의도가 명확하다면, '스크랩'만 확장 매칭으로 두면 됩니다.

   {
     label: '스크랩',
     path: '/mytodo/scrap',
+    match: (pathname: string) => pathname.startsWith('/mytodo/scrap'),
   },
src/hook/community/useDeleteCommunityTodos.ts (3)

25-32: invalidate 후 즉시 refetch 중복 호출 제거

React Query v4에서는 invalidateQueries가 활성 쿼리 자동 리패치를 트리거합니다. invalidate와 refetch를 동시에 호출하면 네트워크 호출이 2번 발생할 수 있습니다. active만 리패치하도록 invalidate 옵션만 두는 것을 권장합니다.

     onSuccess: () => {
-      queryClient.invalidateQueries({
-        queryKey: ['CommunityGetTodo'],
-      });
-      queryClient.refetchQueries({
-        queryKey: ['CommunityGetTodo'],
-      });
+      queryClient.invalidateQueries({
+        queryKey: ['CommunityGetTodo'],
+        refetchType: 'active',
+      });
     },

11-23: mutationKey/타이핑 추가로 디버깅 및 안정성 향상

명시적인 mutationKey와 제네릭을 추가하면 Devtools 식별성과 타입 안정성이 좋아집니다. 응답 타입을 아직 모르면 unknown으로 시작해도 됩니다.

-  return useMutation({
+  return useMutation<unknown, Error, DeleteCommunityTodo>({
+    mutationKey: ['CommunityDeleteTodo'],
     mutationFn: async ({ id }: DeleteCommunityTodo) => {

18-21: DELETE 본문 전송 필요 여부 확인

경로 파라미터로 id를 전달하고 있어 body의 { id }는 중복일 수 있습니다. 서버가 DELETE body를 무시/차단하는 경우가 있으니 API 스펙을 확인해 주세요. 불필요하면 제거하는 편이 안전합니다.

-      const response = await api.delete(`/v1/community/todos/${id}`, {
-        headers: { Authorization: `Bearer ${token}` },
-        data: { id },
-      });
+      const response = await api.delete(`/v1/community/todos/${id}`, {
+        headers: { Authorization: `Bearer ${token}` },
+      });
src/hook/community/query/useCommunityGetTodo.ts (2)

36-42: 요청 취소(AbortSignal) 연동으로 낭비되는 요청 줄이기

파라미터 변경 시 이전 요청을 취소하지 않아 레이스/불필요 응답 처리 가능성이 있습니다. React Query가 제공하는 signal을 axios에 전달하세요. (axios 최신 버전은 signal을 지원)

-const fetchCommunityPage = async ({
-  jobName,
-  level,
-  sort,
-  page,
-  size,
-}: BaseParams & { page: number }): Promise<CommunityGetTodoResponse> => {
+const fetchCommunityPage = async ({
+  jobName,
+  level,
+  sort,
+  page,
+  size,
+  signal,
+}: BaseParams & { page: number; signal?: AbortSignal }): Promise<CommunityGetTodoResponse> => {
   const token =
     typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null;

-  const res = await api.get('/v1/community/todos', {
-    params: { jobName, level, sort, page, size },
-    ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
-  });
+  const params: Record<string, string | number> = { sort, page, size };
+  if (jobName) params.jobName = jobName;
+  if (level) params.level = level;
+  const res = await api.get('/v1/community/todos', {
+    params,
+    signal,
+    ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
+  });

 export const useCommunityGetTodo = (params: BaseParams) => {
   return useInfiniteQuery({
     queryKey: ['CommunityGetTodo', params],
     initialPageParam: 0,
-    queryFn: ({ pageParam }) =>
-      fetchCommunityPage({ ...params, page: pageParam as number }),
+    queryFn: ({ pageParam, signal }) =>
+      fetchCommunityPage({ ...params, page: pageParam as number, signal }),

Also applies to: 46-49, 69-71


77-78: 재시도 정책 재검토 제안

retry: 0은 일시적 네트워크 글리치에도 즉시 실패합니다. UX 관점에서 1회 정도의 자동 재시도를 권장합니다.

-    retry: 0,
+    retry: 1,
src/hook/community/useCommunityAddTodoMutation.ts (4)

24-24: mutationKey 지정으로 디버깅/추적성 향상

뮤테이션 키를 부여하면 Devtools/로깅에서 추적이 쉬워집니다.

   >({
+    mutationKey: ['CommunityAddTodo'],
     mutationFn: async ({ id }) => {

25-28: SSR 안전성: localStorage 접근 가드

클라이언트에서만 실행되겠지만 방어적으로 typeof window 체크를 권장합니다.

-      const token = localStorage.getItem('accessToken');
+      const token =
+        typeof window !== 'undefined'
+          ? localStorage.getItem('accessToken')
+          : null;
       if (!token) {
         throw new Error('인증 토큰이 없습니다. 로그인 후 다시 시도해주세요.');
       }

30-38: 중복 파라미터 전송 여부 확인 (경로 + 바디 모두 id 포함)

/v1/community/todos/${id} 경로에 이미 ID가 있는데 바디에도 { id }를 전송합니다. 백엔드 스펙상 바디가 불필요하다면 제거해 불일치 리스크를 줄이세요.

필요 시 삭제 뮤테이션(useDeleteCommunityTodos.ts)과 동일한 규약으로 정렬하도록 PR에 포함해드릴 수 있습니다.


47-49: 로그 메시지 톤/용어 통일

콘솔 에러 "할일 추가 실패" 표현을 다른 화면/뮤테이션들과 용어 일관되게 맞추면 디버깅 시 편합니다. (예: “내 할 일 추가 실패”)

src/pages/community/Community.tsx (1)

6-6: 레이아웃 반응형 여백/최대폭 가드 제안

데스크톱 기준 px-[120px] 고정 패딩은 좁은 뷰포트에서 깨질 수 있습니다. 컨테이너 최대폭/중앙정렬을 추가하면 안전합니다.

-    <div className="flex h-full w-full gap-[22px] bg-gray-50 px-[120px]">
+    <div className="mx-auto flex h-full w-full max-w-screen-xl gap-[22px] bg-gray-50 px-6 md:px-[120px]">
src/pages/community/components/CommunityRightSide.tsx (3)

86-102: 드롭다운 옵션 a11y 개선: div 대신 button 사용

키보드 접근성/스크린리더 친화성을 위해 클릭용 요소는 button으로 바꾸세요.

-              {sortOptions.map((option) => (
-                <div
-                  key={option}
-                  onClick={() => handleSelect(option)}
-                  className={`w-full cursor-pointer px-5 py-6 text-left ${
+              {sortOptions.map((option) => (
+                <button
+                  type="button"
+                  key={option}
+                  onClick={() => handleSelect(option)}
+                  className={`w-full cursor-pointer px-5 py-6 text-left ${
                     sort === option
                       ? 'text-purple-500 font-B01-SB'
                       : 'text-gray-400 font-B01-M'
                   }`}
                 >
                   {option}
-                </div>
+                </button>
               ))}

113-116: 빈 리스트 상태 처리 추가

데이터가 비어있고 에러/로딩이 아닐 때의 UI가 없습니다. 사용자에게 명확히 알려주세요.

-      {items.length > 0 && (
+      {items.length > 0 ? (
         <CommunityContents items={items} activeLevel={active} sort={sort} />
-      )}
+      ) : (
+        !isLoading &&
+        !isError && (
+          <p className="mt-6 text-center text-gray-400">표시할 할 일이 없습니다.</p>
+        )
+      )}

59-71: 레벨 라벨/API 값 분리 유지, 소스 상수 중앙화 제안

levels 상수는 잘 구성되어 있습니다. 동일 매핑을 다른 모듈(예: 훅/서버 DTO)에서도 쓰는 경우 공용 상수로 분리하면 드리프트를 줄일 수 있습니다.

원하시면 src/constants/community.ts로 분리 패치 생성해드리겠습니다.

src/pages/community/components/CommunityLeftSide.tsx (4)

16-23: Dropdown 초기값/로깅 정리

  • value={jobNames[0]}는 Dropdown 내부의 API-초기화 로직과 충돌 소지가 있습니다(불필요한 플래시/덮어쓰기).
  • console.log는 제거하세요.
-        <CommunityDropdown
-          options={jobNames}
-          value={jobNames[0]}
-          onSelect={(value) => {
-            console.log(value);
-          }}
-          className="cursor-pointer"
-        />
+        <CommunityDropdown
+          options={jobNames}
+          onSelect={() => {}}
+          className="cursor-pointer"
+        />

27-37: “전체보기” 접근성/안정성 개선

  • div 클릭 대신 버튼 역할/키보드 접근을 부여하고, jobId가 없을 때 더 안전한 폴백을 사용하세요(첫 직무의 id → 끝으로 1).
-        <div
-          className="mt-[30px] flex w-full cursor-pointer flex-row items-center justify-end text-gray-500 font-B02-SB"
-          onClick={() => {
-            // 하드코딩 매핑된 jobId 사용
-            const id = findJobIdByName(selectedJobName);
-            navigate(`/others/${id ?? 1}`);
-          }}
-        >
+        <div
+          role="button"
+          tabIndex={0}
+          className="mt-[30px] flex w-full cursor-pointer flex-row items-center justify-end text-gray-500 font-B02-SB"
+          onClick={() => {
+            const id =
+              findJobIdByName(selectedJobName) ??
+              (jobNames.length ? findJobIdByName(jobNames[0]) ?? undefined : undefined);
+            navigate(`/others/${id ?? 1}`);
+          }}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') {
+              const id =
+                findJobIdByName(selectedJobName) ??
+                (jobNames.length ? findJobIdByName(jobNames[0]) ?? undefined : undefined);
+              navigate(`/others/${id ?? 1}`);
+            }
+          }}
+        >

41-44: 제목 빈 값 폴백

초기 진입 시 selectedJobName이 비어 공백만 표시됩니다. 가독성을 위해 폴백을 두세요.

-          {selectedJobName} HOT 할 일
+          {(selectedJobName || '전체')} HOT 할 일

54-58: 이미지 a11y/성능 마이너 개선

  • alt에 사용자 이름을 사용하고, lazy 로딩을 추가하세요.
-                  <img
-                    src={item.imageUrl}
-                    alt="프로필이미지"
-                    className="h-[30px] w-[30px] rounded-full bg-gray-50"
-                  />
+                  <img
+                    src={item.imageUrl}
+                    alt={item.name || '프로필 이미지'}
+                    loading="lazy"
+                    className="h-[30px] w-[30px] rounded-full bg-gray-50 object-cover"
+                  />
src/pages/community/components/CommunityContents.tsx (7)

1-1: CommunityItem 타입 중복 정의 제거(단일 소스 유지)

동일 타입을 로컬에서 재정의하면 드리프트 위험이 큽니다. 기존 정의를 import하여 단일 소스로 유지하세요.

-import { useState, useMemo } from 'react';
+import { useState, useMemo } from 'react';
+import type { CommunityItem } from '@hook/community/query/useCommunityGetTodo';
@@
-type CommunityItem = {
-  id: number;
-  name: string;
-  level: string;
-  imageUrl?: string;
-  dDay?: string;
-  description: string;
-  saveCount: number;
-  isSaved: boolean;
-};
+// 기존 정의를 사용합니다: import type { CommunityItem } ...

Also applies to: 9-18


45-49: ‘최신순’ 정렬이 구현되지 않았습니다

현재는 ‘인기순’만 정렬되고 ‘최신순’은 원본 순서를 그대로 사용합니다. 서버 반환 순서에 의존하지 말고 명시적인 기준 필드를 정렬에 사용하세요(예: createdAt).

가능 시 아래처럼 정렬 필드 확인 후 반영해 주세요:

   const sorted = useMemo(() => {
     const base = [...filtered];
-    if (sort === '인기순') base.sort((a, b) => b.saveCount - a.saveCount);
+    if (sort === '인기순') base.sort((a, b) => b.saveCount - a.saveCount);
+    else if (sort === '최신순') {
+      // TODO: createdAt(ISO string/number) 존재 시 내림차순 정렬
+      // base.sort((a, b) => new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime());
+    }
     return base;
   }, [filtered, sort]);

필드 명/타입을 알려주시면 정렬 로직을 확정해 드리겠습니다.


60-61: setTimeout 메모리 릭/언마운트 경합 방지

언마운트 시 타이머가 남아있을 수 있습니다. ref로 타이머를 관리하고 cleanup을 추가하세요.

-import { useState, useMemo } from 'react';
+import { useState, useMemo, useRef, useEffect } from 'react';
@@
-  const [showToast, setShowToast] = useState(false);
+  const [showToast, setShowToast] = useState(false);
   const [toastMessage, setToastMessage] = useState('');
+  const toastTimerRef = useRef<number | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (toastTimerRef.current) {
+        clearTimeout(toastTimerRef.current);
+      }
+    };
+  }, []);
@@
-            setShowToast(true);
-            setTimeout(() => setShowToast(false), 2500);
+            setShowToast(true);
+            if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
+            toastTimerRef.current = window.setTimeout(() => setShowToast(false), 2500);
@@
-            setShowToast(true);
+            setShowToast(true);
             setAdded((prev) => ({ ...prev, [id]: true }));
             setToastMessage('할일이 추가되었습니다.');
-            setTimeout(() => {
-              setShowToast(false);
-            }, 2500);
+            if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
+            toastTimerRef.current = window.setTimeout(() => setShowToast(false), 2500);

Also applies to: 75-77


132-142: 중복 클릭 방지: 변이 진행 중 버튼 비활성화

추가/취소 요청 중 중복 요청을 방지하세요.

-              <button
+              <button
                 type="button"
-                onClick={() => toggleAdd(post.id, isAdded)}
+                onClick={() => toggleAdd(post.id, isAdded)}
+                disabled={addTodoMutation.isPending || deleteTodoMutation.isPending}
+                aria-busy={addTodoMutation.isPending || deleteTodoMutation.isPending}
                 className={
                   isAdded
                     ? 'p-2 text-purple-500 font-B03-SB'
                     : 'flex items-center justify-center rounded-[10px] bg-purple-500 p-2 text-purple-50 font-B03-SB'
                 }
               >

62-64: alert 대신 일관된 토스트 UI 사용

에러도 ToastModal로 노출하면 UX 일관성이 좋아집니다.

-          onError: () => {
-            alert('추가 취소에 실패했어요.');
-          },
+          onError: () => {
+            setToastMessage('추가 취소에 실패했어요.');
+            setShowToast(true);
+          },
@@
-          onError: () => {
-            alert('내 할일 추가에 실패했어요.');
-          },
+          onError: () => {
+            setToastMessage('내 할일 추가에 실패했어요.');
+            setShowToast(true);
+          },

Also applies to: 79-81


87-93: 로딩 오버레이 접근성 보완

보조기기 인지를 위해 role/aria 속성을 추가하세요.

-      <div className="fixed inset-0 z-50 flex items-center justify-center">
+      <div className="fixed inset-0 z-50 flex items-center justify-center" role="status" aria-live="polite" aria-busy="true">
         <LoadingSpinner />
       </div>

174-182: 토스트 위치 고정 시 중앙 정렬 보완

현재 items-center만으로는 중앙 배치가 되지 않습니다.

-        <div className="fixed top-[100px] z-50 items-center">
+        <div className="fixed top-[100px] left-1/2 z-50 -translate-x-1/2 transform">
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 03eadb5 and e14c43a.

📒 Files selected for processing (11)
  • src/common/Header.tsx (1 hunks)
  • src/hook/community/query/useCommunityGetTodo.ts (1 hunks)
  • src/hook/community/useCommunityAddTodoMutation.ts (1 hunks)
  • src/hook/community/useDeleteCommunityTodos.ts (1 hunks)
  • src/hook/community/useInfinityScroll.ts (1 hunks)
  • src/pages/community/Community.tsx (1 hunks)
  • src/pages/community/components/CommunityContents.tsx (2 hunks)
  • src/pages/community/components/CommunityLeftSide.tsx (1 hunks)
  • src/pages/community/components/CommunityRightSide.tsx (1 hunks)
  • src/pages/myTodo/MyTodoPage.tsx (1 hunks)
  • src/pages/myTodo/components/scrap/ScrapEmptyState.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/pages/community/components/CommunityLeftSide.tsx (4)
src/store/useCommunityStore.ts (1)
  • useCommunityStore (9-14)
src/hook/community/query/useGetHotPopularQuery.ts (1)
  • useGetHotPopularQuery (22-39)
src/pages/community/components/CommunityDropdown.tsx (1)
  • CommunityDropdown (17-113)
src/utils/data/community/jobs.ts (2)
  • jobNames (29-29)
  • findJobIdByName (31-34)
src/pages/community/components/CommunityRightSide.tsx (3)
src/store/useCommunityStore.ts (1)
  • useCommunityStore (9-14)
src/hook/community/query/useCommunityGetTodo.ts (1)
  • useCommunityGetTodo (65-79)
src/hook/community/useInfinityScroll.ts (1)
  • useInfiniteScroll (12-60)
src/pages/community/components/CommunityContents.tsx (3)
src/hook/community/query/useCommunityGetTodo.ts (1)
  • CommunityItem (4-13)
src/hook/community/useCommunityAddTodoMutation.ts (1)
  • useCommunityAddTodoMutation (16-51)
src/hook/community/useDeleteCommunityTodos.ts (1)
  • useDeleteCommunityTodosMutation (8-38)
🔇 Additional comments (1)
src/pages/community/components/CommunityRightSide.tsx (1)

44-48: 무한스크롤 트리거 안전장치 점검

enabled: !!hasNextPage && !isFetching && !isLoading && !isError는 적절합니다. 다만 fetch 실패 후 재시작 조건(에러 해제 시)도 정상 동작하는지 QA에서 확인 부탁드립니다.

Comment on lines +46 to +49
const res = await api.get('/v1/community/todos', {
params: { jobName, level, sort, page, size },
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

빈 문자열 파라미터 전송 방지 (필터 무효화 이슈 예방)

jobName/level이 빈 문자열일 때도 그대로 쿼리에 붙어 서버가 “빈 문자열”로 필터링할 수 있습니다. 미설정인 경우 파라미터 자체를 제거하세요.

-  const res = await api.get('/v1/community/todos', {
-    params: { jobName, level, sort, page, size },
-    ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
-  });
+  const params: Record<string, string | number> = { sort, page, size };
+  if (jobName) params.jobName = jobName;
+  if (level) params.level = level;
+
+  const res = await api.get('/v1/community/todos', {
+    params,
+    ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
+  });
📝 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 res = await api.get('/v1/community/todos', {
params: { jobName, level, sort, page, size },
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
// Prevent sending empty-string filters, which could invalidate server-side filtering
const params: Record<string, string | number> = { sort, page, size };
if (jobName) params.jobName = jobName;
if (level) params.level = level;
const res = await api.get('/v1/community/todos', {
params,
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
🤖 Prompt for AI Agents
In src/hook/community/query/useCommunityGetTodo.ts around lines 46 to 49, the
request always includes jobName and level even when they are empty strings which
can cause the backend to treat them as explicit filters; build the params object
by only adding jobName and level when they are non-empty (e.g., check for !== ''
or truthiness) along with page, size and sort, so that empty-string values are
omitted from the query; keep the conditional Authorization header logic as-is.

Comment on lines +50 to +55
const result = res.data?.data;

return {
content: result.content,
number: result.number,
size: result.size,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

응답 안전성 가드 추가

res.data?.data가 없거나 content가 배열이 아니면 이후 접근에서 런타임 오류가 납니다. 즉시 명확한 에러를 던져 React Query가 핸들하도록 하세요.

-  const result = res.data?.data;
+  const result = res.data?.data;
+  if (!result || !Array.isArray(result.content)) {
+    throw new Error('커뮤니티 투두 응답 형식이 올바르지 않습니다.');
+  }
📝 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 result = res.data?.data;
return {
content: result.content,
number: result.number,
size: result.size,
const result = res.data?.data;
if (!result || !Array.isArray(result.content)) {
throw new Error('커뮤니티 투두 응답 형식이 올바르지 않습니다.');
}
return {
content: result.content,
number: result.number,
size: result.size,
🤖 Prompt for AI Agents
In src/hook/community/query/useCommunityGetTodo.ts around lines 50 to 55,
res.data?.data is used without validation which can cause runtime errors if it's
missing or if content is not an array; add an explicit guard: check that
res.data?.data exists and that Array.isArray(res.data.data.content) (or the
expected type) is true, and if not throw a clear Error (e.g. "Invalid response:
missing data.content") so React Query can handle the failure; then safely return
result.content, number, and size after the validation.

Comment on lines +43 to +45
queryClient.invalidateQueries({
queryKey: ['CommunityGetTodo', 'mdTodo', 'community'],
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿼리 무효화 키 불일치: 리스트 갱신이 안 될 수 있습니다

조회 훅의 키는 ['CommunityGetTodo', params]인데, 무효화는 ['CommunityGetTodo', 'mdTodo', 'community']로 되어 있어 매치되지 않을 가능성이 큽니다. 프리픽스 기준으로 전체를 무효화하세요.

-      queryClient.invalidateQueries({
-        queryKey: ['CommunityGetTodo', 'mdTodo', 'community'],
-      });
+      queryClient.invalidateQueries({
+        queryKey: ['CommunityGetTodo'],
+      });
📝 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
queryClient.invalidateQueries({
queryKey: ['CommunityGetTodo', 'mdTodo', 'community'],
});
queryClient.invalidateQueries({
queryKey: ['CommunityGetTodo'],
});
🤖 Prompt for AI Agents
In src/hook/community/useCommunityAddTodoMutation.ts around lines 43 to 45, the
invalidateQueries call uses the wrong key ['CommunityGetTodo', 'mdTodo',
'community'] which won't match the reader hook's key ['CommunityGetTodo',
params]; replace the invalidate call to invalidate by the prefix used by the
query (e.g. queryClient.invalidateQueries(['CommunityGetTodo']) or otherwise
pass the same params object) so the list query is correctly invalidated and
refreshed.

Comment on lines +20 to +53
const ref = useRef<T | null>(null);
const calledOnceRef = useRef(false);

const handleIntersect = useCallback(
(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
const entry = entries[0];
if (!entry?.isIntersecting) return;
if (once && calledOnceRef.current) return;

onIntersect();

if (once) {
calledOnceRef.current = true;
observer.unobserve(entry.target);
}
},
[onIntersect, once]
);

useEffect(() => {
const el = ref.current;
if (!enabled || !el) return;

if (typeof IntersectionObserver === 'undefined') return;

const observer = new IntersectionObserver(handleIntersect, {
root,
rootMargin,
threshold,
});

observer.observe(el);
return () => observer.disconnect();
}, [enabled, root, rootMargin, threshold, handleIntersect]);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

DOM 노드 교체 시 옵저버가 새 요소를 관찰하지 않는 문제

현재 구현은 ref.current로 최초 요소만 observe합니다. 무한스크롤 sentinel이 리렌더로 교체되면 기존 옵저버가 새 노드를 관찰하지 않아 페이징이 멈출 수 있습니다. 콜백 ref로 노드 교체를 안전하게 처리하는 패턴을 권장합니다. 반환 타입이 callback ref로 바뀌므로 사용처에서 ref={returnedRef} 형태인지 확인 부탁드립니다.

   const ref = useRef<T | null>(null);
   const calledOnceRef = useRef(false);
+  const observerRef = useRef<IntersectionObserver | null>(null);
+  const targetRef = useRef<T | null>(null);
 
   const handleIntersect = useCallback(
     (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
       const entry = entries[0];
       if (!entry?.isIntersecting) return;
       if (once && calledOnceRef.current) return;
 
       onIntersect();
 
       if (once) {
         calledOnceRef.current = true;
         observer.unobserve(entry.target);
       }
     },
     [onIntersect, once]
   );
 
-  useEffect(() => {
-    const el = ref.current;
-    if (!enabled || !el) return;
-
-    if (typeof IntersectionObserver === 'undefined') return;
-
-    const observer = new IntersectionObserver(handleIntersect, {
-      root,
-      rootMargin,
-      threshold,
-    });
-
-    observer.observe(el);
-    return () => observer.disconnect();
-  }, [enabled, root, rootMargin, threshold, handleIntersect]);
+  const setRef = useCallback((node: T | null) => {
+    // 외부에서 필요하면 접근 가능하도록 유지
+    ref.current = node;
+    if (observerRef.current && targetRef.current) {
+      observerRef.current.unobserve(targetRef.current);
+    }
+    targetRef.current = node;
+    if (!enabled || !node) return;
+    if (typeof IntersectionObserver === 'undefined') return;
+    if (!observerRef.current) {
+      observerRef.current = new IntersectionObserver(handleIntersect, {
+        root,
+        rootMargin,
+        threshold,
+      });
+    }
+    observerRef.current.observe(node);
+  }, [enabled, root, rootMargin, threshold, handleIntersect]);
+
+  useEffect(() => {
+    // enabled false로 전환 시 정리
+    if (!enabled && observerRef.current) {
+      observerRef.current.disconnect();
+      observerRef.current = null;
+      targetRef.current = null;
+    }
+  }, [enabled]);
+
+  useEffect(() => {
+    // 언마운트 정리
+    return () => {
+      if (observerRef.current) observerRef.current.disconnect();
+    };
+  }, []);
 
   useEffect(() => {
     if (!enabled) calledOnceRef.current = false;
   }, [enabled]);
 
-  return ref;
+  return setRef;

필수: 콜백 ref로 sentinel 교체 처리
현재 useRef로 최초 요소만 observe하고 있어, sentinel이 리렌더로 교체될 때 새 요소를 관찰하지 않습니다. 이전 노드를 unobserve한 뒤 새 노드를 observe하도록 callback ref(setRef) 패턴으로 변경해야 합니다.

🤖 Prompt for AI Agents
In src/hook/community/useInfinityScroll.ts around lines 20 to 53, the hook
currently uses a static useRef for the sentinel so when that DOM node is
replaced by a re-render the new node is never observed; replace the ref with a
callback ref (setRef) that tracks the previous element, unobserves it if
present, assigns the new element, and observes the new element with the existing
IntersectionObserver logic (create or reuse observer), and ensure cleanup by
disconnecting or unobserving in the callback and in useEffect cleanup so the
hook correctly observes new sentinels on re-renders.

Comment on lines +51 to +85
const toggleAdd = (id: number, isAdded: boolean) => {
if (isAdded) {
deleteTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setAdded((prev) => ({ ...prev, [id]: false }));
setToastMessage('할일이 취소되었습니다.');
setShowToast(true);
setTimeout(() => setShowToast(false), 2500);
},
onError: () => {
alert('추가 취소에 실패했어요.');
},
}
);
} else {
addTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setShowToast(true);
setAdded((prev) => ({ ...prev, [id]: true }));
setToastMessage('할일이 추가되었습니다.');
setTimeout(() => {
setShowToast(false);
}, 2500);
},
onError: () => {
alert('내 할일 추가에 실패했어요.');
},
}
);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

추가/취소 후 캐시 무효화 키 불일치로 인한 UI 갱신 실패 가능성

useCommunityAddTodoMutationonSuccess['CommunityGetTodo', 'mdTodo', 'community']를 무효화하지만, 조회 쿼리는 ['CommunityGetTodo']를 사용합니다. 추가 후 목록이 갱신되지 않을 수 있습니다. 삭제 쪽은 ['CommunityGetTodo']로 일치합니다.

아래처럼 add-mutation의 무효화 키를 조회 키와 일치시켜 주세요.

해당 파일: src/hook/community/useCommunityAddTodoMutation.ts

-      queryClient.invalidateQueries({
-        queryKey: ['CommunityGetTodo', 'mdTodo', 'community'],
-      });
+      queryClient.invalidateQueries({ queryKey: ['CommunityGetTodo'] });
+      // 일관성 강화를 원하면 refetch까지:
+      // queryClient.refetchQueries({ queryKey: ['CommunityGetTodo'] });
📝 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 toggleAdd = (id: number, isAdded: boolean) => {
if (isAdded) {
deleteTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setAdded((prev) => ({ ...prev, [id]: false }));
setToastMessage('할일이 취소되었습니다.');
setShowToast(true);
setTimeout(() => setShowToast(false), 2500);
},
onError: () => {
alert('추가 취소에 실패했어요.');
},
}
);
} else {
addTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setShowToast(true);
setAdded((prev) => ({ ...prev, [id]: true }));
setToastMessage('할일이 추가되었습니다.');
setTimeout(() => {
setShowToast(false);
}, 2500);
},
onError: () => {
alert('내 할일 추가에 실패했어요.');
},
}
);
}
};
// src/hook/community/useCommunityAddTodoMutation.ts
export const useCommunityAddTodoMutation = () => {
const queryClient = useQueryClient();
return useMutation(addTodoApiCall, {
onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: ['CommunityGetTodo', 'mdTodo', 'community'],
queryClient.invalidateQueries({ queryKey: ['CommunityGetTodo'] });
// 일관성 강화를 원하면 refetch까지:
// queryClient.refetchQueries({ queryKey: ['CommunityGetTodo'] });
},
onError: () => {
// error handling...
},
});
};
🤖 Prompt for AI Agents
In src/pages/community/components/CommunityContents.tsx lines 51-85: the
add-todo mutation used here relies on useCommunityAddTodoMutation which
currently invalidates ['CommunityGetTodo', 'mdTodo', 'community'] while the
component's read query uses ['CommunityGetTodo'], causing the UI to not refresh
after add; open src/hook/community/useCommunityAddTodoMutation.ts and change the
invalidateQueries call in the mutation's onSuccess to use the exact key
['CommunityGetTodo'] (or otherwise match the read query key), so the list query
is correctly invalidated and the UI updates; ensure you keep other onSuccess
behavior intact and only adjust the invalidate key to match the component's
query.

Comment on lines +19 to 21
const sortOptions: Sort[] = ['최신순', '인기순'];
const toApiLevel = (v: Level) => levels.find((l) => l.value === v)?.api ?? '';

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 매핑 추가

UI 라벨(‘최신순’, ‘인기순’)을 그대로 API에 전달하면 서버 기대값(예: 'LATEST'/'POPULAR')과 불일치할 수 있습니다. 명시적 매핑을 둬 안정화하세요.

 type Sort = '최신순' | '인기순';

 const levels: { value: Level; label: string; api: string }[] = [
@@
 const sortOptions: Sort[] = ['최신순', '인기순'];
 const toApiLevel = (v: Level) => levels.find((l) => l.value === v)?.api ?? '';
+const toApiSort = (s: Sort) => (s === '최신순' ? 'LATEST' : 'POPULAR');
@@
   const { data, isLoading, isError, isFetching, fetchNextPage, hasNextPage } =
     useCommunityGetTodo({
       jobName: selectedJobName,
       level: toApiLevel(active),
-      sort,
+      sort: toApiSort(sort),
       size,
     });

Also applies to: 31-37

🤖 Prompt for AI Agents
In src/pages/community/components/CommunityRightSide.tsx around lines 19-21 and
also affecting lines 31-37, the UI sort labels ('최신순', '인기순') are being passed
directly to the API which mismatches the server's expected values; add an
explicit mapping object (or switch) that maps each Sort label to the API sort
key (e.g., '최신순' => 'LATEST', '인기순' => 'POPULAR'), implement a toApiSort
function that returns the mapped value with a safe default, and replace any
direct uses of the UI label when building API requests to use this mapping
function instead.

Comment on lines +106 to +111
{isLoading && isFetching && items.length > 0 && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
{isError && <p className="mt-6 text-red-500">에러가 발생했습니다</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

로딩 스피너 조건 버그: 실제로는 표시되지 않습니다

isLoading && isFetching && items.length > 0는 동시에 만족하기 어렵습니다. “첫 페이지 로딩”과 “추가 페이지 로딩(오버레이)”를 분리하세요.

-      {isLoading && isFetching && items.length > 0 && (
-        <div className="fixed inset-0 z-50 flex items-center justify-center">
-          <LoadingSpinner />
-        </div>
-      )}
+      {isLoading && items.length === 0 && (
+        <div className="flex items-center justify-center py-16">
+          <LoadingSpinner />
+        </div>
+      )}
+      {isFetching && items.length > 0 && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center">
+          <LoadingSpinner />
+        </div>
+      )}
📝 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
{isLoading && isFetching && items.length > 0 && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
{isError && <p className="mt-6 text-red-500">에러가 발생했습니다</p>}
{isLoading && items.length === 0 && (
<div className="flex items-center justify-center py-16">
<LoadingSpinner />
</div>
)}
{isFetching && items.length > 0 && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
{isError && <p className="mt-6 text-red-500">에러가 발생했습니다</p>}
🤖 Prompt for AI Agents
In src/pages/community/components/CommunityRightSide.tsx around lines 106 to
111, the current condition `isLoading && isFetching && items.length > 0` makes
the spinner almost never show; split the logic into two clear cases: show a
full-page or inline initial loader when `isLoading` (and items.length === 0) for
first-page load, and show the overlay/mini loader when `isFetching &&
items.length > 0` for paginated/further fetches; update the JSX to use those two
separate conditions so the spinner appears correctly in both scenarios.

@Chasyuss Chasyuss merged commit 108ed15 into develop Sep 5, 2025
1 of 2 checks passed
This was referenced Sep 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants