Skip to content

[feat] 주제 조회 UI 및 기능 개발#61

Merged
haruyam15 merged 7 commits intodevelopfrom
feat/topics-list-46
Feb 7, 2026
Merged

[feat] 주제 조회 UI 및 기능 개발#61
haruyam15 merged 7 commits intodevelopfrom
feat/topics-list-46

Conversation

@haruyam15
Copy link
Contributor

@haruyam15 haruyam15 commented Feb 6, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

  • 약속 상세 페이지에 주제 목록 기능 추가
  • 확정된 주제(Confirmed)와 제안된 주제(Proposed) 탭으로 분리하여 표시
  • 무한 스크롤 적용
  • 주제 좋아요/삭제 기능 구현

🔧 변경 사항

컴포넌트

  • TopicHeader: 주제 탭 전환 및 헤더 UI
  • ConfirmedTopicList / ProposedTopicList: 각 탭별 주제 목록
  • TopicCard: 개별 주제 카드 (좋아요, 삭제 등 상호작용 포함)
  • TopicListSkeleton: 로딩 상태 스켈레톤
  • EmptyTopicList: 빈 상태 UI

데이터 레이어

  • TanStack Query를 활용한 서버 상태 관리
  • useConfirmedTopics / useProposedTopics: 무한 스크롤 쿼리
  • useLikeTopic / useDeleteTopic: 뮤테이션 훅
  • 좋아요는 Optimistic Update로 즉각적인 UI 반영

성능 최적화

  • 무한 스크롤로 대량 데이터 효율적 로딩
  • 쿼리 키 관리 체계화 (topicQueryKeys)

기타

  • 페이지네이션 상수 정의 추가
  • LikeButton 컴포넌트 개선
  • Mock 데이터 및 API 엔드포인트 정의

📸 스크린샷 (선택 사항)

image

Summary by CodeRabbit

  • 새로운 기능

    • 모임 내 PROPOSED / CONFIRMED 탭 인터페이스 추가
    • 제안/확정된 주제 목록에 무한 스크롤 페이징 적용
    • 주제별 좋아요 토글 및 삭제 기능 추가
    • 기본 주제 카드, 빈 목록 메시지, 로딩 스켈레톤 UI 제공
  • 개선사항

    • 좋아요 버튼의 처리 중(isPending) 상태에 대한 시각적·기능적 피드백 개선

- 확정된 주제와 제안된 주제를 분리하여 표시하는 목록 컴포넌트 추가
- 무한 스크롤 및 가상화(@tanstack/react-virtual)를 통한 성능 최적화
- 주제 좋아요, 삭제 기능 구현
- 빈 상태 및 로딩 스켈레톤 UI 추가
- TanStack Query를 활용한 서버 상태 관리
@haruyam15 haruyam15 self-assigned this Feb 6, 2026
@haruyam15 haruyam15 added the feat 새로운 기능 추가 label Feb 6, 2026
@haruyam15 haruyam15 linked an issue Feb 6, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Walkthrough

주제(Topics) 기능을 신규 추가합니다. 컴포넌트(7개), 훅(5개), API 래퍼·엔드포인트·모킹·타입, MeetingDetailPage 탭 통합 및 무한 스크롤 연동을 포함합니다. (요약 45단어)

Changes

Cohort / File(s) Summary
Topic Components
src/features/topics/components/ConfirmedTopicList.tsx, src/features/topics/components/ProposedTopicList.tsx, src/features/topics/components/TopicCard.tsx, src/features/topics/components/TopicHeader.tsx, src/features/topics/components/DefaultTopicCard.tsx, src/features/topics/components/EmptyTopicList.tsx, src/features/topics/components/TopicListSkeleton.tsx, src/features/topics/components/index.ts
7개 컴포넌트 추가. 리스트 렌더링·무한스크롤 관찰자 통합(Confirmed/Proposed), TopicCard에 좋아요/삭제 뮤테이션 및 확인 모달 연동, 스켈레톤과 기본 카드 추가.
Topic Hooks
src/features/topics/hooks/useProposedTopics.ts, src/features/topics/hooks/useConfirmedTopics.ts, src/features/topics/hooks/useLikeTopic.ts, src/features/topics/hooks/useDeleteTopic.ts, src/features/topics/hooks/topicQueryKeys.ts, src/features/topics/hooks/index.ts
React Query 기반 무한쿼리 훅(제안/확정), 좋아요(낙관적 업데이트)/삭제 뮤테이션, 쿼리키 팩토리 및 훅 배럴 추가.
API / Endpoints / Mock / Types
src/features/topics/topics.api.ts, src/features/topics/topics.endpoints.ts, src/features/topics/topics.mock.ts, src/features/topics/topics.types.ts, src/features/topics/index.ts
토픽 API 래퍼(getProposed/getConfirmed/delete/likeToggle), 엔드포인트 상수, 대량 모킹 데이터 및 페이징 헬퍼, 타입 정의(아이템·커서·요청·응답).
Page Integration
src/pages/Meetings/MeetingDetailPage.tsx
MeetingDetailPage에 PROPOSED/CONFIRMED 탭 추가 및 각 탭에 TopicHeader와 리스트 컴포넌트(무한스크롤 훅 연동) 삽입. 로딩/에러 처리와 탭 카운트 바인딩 포함.
Shared Changes
src/shared/ui/LikeButton.tsx, src/shared/constants/pagination.ts
LikeButton에 isPending 추가로 뮤테이션 대기 상태에서 버튼 비활성화 처리. PAGE_SIZES에 TOPICS: 5 추가.
CI / Manifest
.github/workflows/deploy.yml, manifest_file, package.json
빌드 환경에 VITE_KAKAO_MAP_KEY 추가 및 패키지/매니페스트 라인 변경(추적용).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as MeetingDetailPage / Component
participant Hook as useProposedTopics / useConfirmedTopics
participant API as topics.api
participant Cache as ReactQuery Cache
UI->>Hook: useInfiniteQuery 요청 (gatheringId, meetingId, pageSize)
Hook->>API: getProposedTopics / getConfirmedTopics (cursor?)
API-->>Hook: 페이지 응답(items, nextCursor, hasNext)
Hook->>Cache: 페이지들 저장
UI->>UI: 스크롤 감지 (useInfiniteScroll)
UI->>Hook: onLoadMore 호출 (when sentinel visible)
Hook->>API: 다음 페이지 요청(cursor)
API-->>Hook: 다음 페이지 응답
Note right of Cache: 좋아요/삭제는 뮤테이션 → 낙관적 업데이트 및 캐시 롤백 처리

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • mgYang53
  • choiyoungae
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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
Title check ✅ Passed PR 제목이 주제 조회 UI 및 기능 개발에 대해 명확하게 설명하며, 변경 사항의 핵심을 잘 나타냅니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/topics-list-46

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.

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

🤖 Fix all issues with AI agents
In `@src/features/topics/components/ProposedTopicList.tsx`:
- Around line 28-32: ProposedTopicList (and likewise ConfirmedTopicList) is
using useVirtualList with only estimateSize which breaks layout for
variable-height TopicCard items; update the rendering of the virtual list so
each <li> gets the measureElement ref and a data-index attribute from
virtualItems (i.e., call measureElement as the ref for the list item and set
data-index={virtualItem.index}), ensure you use virtualItems.map(...) to render
items and pass key={virtualItem.key} and style={virtualItem.style} on the <li>
so the virtualizer can measure and position items correctly.

In `@src/features/topics/components/TopicHeader.tsx`:
- Around line 22-27: The issue is that destructuring a discriminated-union prop
in TopicHeader prevents TypeScript from narrowing types based on the
discriminant (activeTab), causing errors when accessing union-specific fields
like actions.canConfirm and actions.canViewPreOpinions; fix it by not
destructuring the union props—accept a single parameter (e.g., props) in
TopicHeader and reference props.activeTab and props.actions so TypeScript can
narrow correctly, or alternatively keep destructuring but perform explicit type
guards on props.activeTab before using actions (e.g., check props.activeTab ===
'...' then use props.actions.canConfirm), ensuring the code paths where
actions.canConfirm and actions.canViewPreOpinions are accessed are guarded by
the correct discriminant check.

In `@src/features/topics/hooks/useDeleteTopic.ts`:
- Around line 43-50: Replace the manual queryKey array passed into
queryClient.invalidateQueries in useDeleteTopic.ts with the topicQueryKeys
factory: call topicQueryKeys.proposedLists({ gatheringId: variables.gatheringId,
meetingId: variables.meetingId }) and pass that as the key (using the top-level
prefix is sufficient because invalidateQueries does prefix matching). Update the
invalidate call that currently references queryClient.invalidateQueries({
queryKey: [...] }) to use the factory result so key shapes remain consistent
with useLikeTopic and other callers.

In `@src/features/topics/hooks/useInfiniteScroll.ts`:
- Around line 1-36: This feature-level useInfiniteScroll duplicates
functionality present in src/shared/hooks/useInfiniteScroll.ts and misses
rootMargin (should default to 200px), isLoading/enabled flags and environment
checks; replace this hook with an import of the shared useInfiniteScroll
implementation (remove the local useInfiniteScroll), ensure caller uses the
shared API (including rootMargin, isLoading, enabled) and stop passing unstable
inline onLoadMore callbacks (either memoize the callback or avoid including
onLoadMore in the hook's dependency array) so the IntersectionObserver
(observerRef) is not recreated unnecessarily.

In `@src/features/topics/hooks/useLikeTopic.ts`:
- Line 48: The optimistic update fails because useLikeTopic builds a manual
queryKey const queryKey = ['topics','proposed','list',{gatheringId,meetingId}]
that omits pageSize while the real cache keys from useProposedTopics include
pageSize; update onMutate in useLikeTopic to match the actual cache keys by
either (A) accepting pageSize in the mutation variables (propagate pageSize from
the parent TopicCard into likeMutation.mutate and use that in
getQueryData/setQueryData), or (B) use getQueriesData() (or
topicQueryKeys.proposedList() helpers) to find and iterate all matching cached
entries and apply the optimistic change to each, or (C) switch to
topicQueryKeys.proposedList(...) if you can derive/track the full key; adjust
references to getQueryData, setQueryData, and useLikeTopic.onMutate accordingly
so keys match the real cache entries.

In `@src/features/topics/hooks/useProposedTopics.ts`:
- Line 50: The queryKey used in useProposedTopics includes pageSize but
useLikeTopic's onMutate calls setQueryData with a key that omits pageSize,
causing optimistic updates to miss the exact cache entry; fix by centralizing
keys (create or import topicQueryKeys key factory) and use it in both
useProposedTopics (for queryKey) and useLikeTopic.onMutate (for setQueryData and
cancelQueries/invalidateQueries) so they produce identical keys (include or
intentionally omit pageSize consistently), ensuring the optimistic update
targets the same cache entry.

In `@src/features/topics/topics.api.ts`:
- Line 38: The JSDoc default for pageSize is inconsistent with the actual
default used in code; update the documentation to match the real default
(PAGE_SIZES.TOPICS = 5). Specifically, change the JSDoc description for the
pageSize param in topics.api.ts (the param documented as "기본값: 10") to "기본값: 5"
and make the same correction in the related comments in topics.types.ts (the
comments around the pageSize fields at the locations referenced). Ensure the
wording references the actual default constant (PAGE_SIZES.TOPICS) so the docs
stay correct if the constant changes.
- Around line 159-174: The mock in likeTopicToggle currently returns a random
liked value which breaks optimistic-update testing; change the mock to toggle
deterministically using the caller-provided current state by extending
LikeTopicParams to include isLiked (or alternatively maintain an internal map
keyed by topicId) and, when USE_MOCK_DATA is true, compute liked = !isLiked and
newCount = isLiked ? Math.max(0, currentCount - 1) : currentCount + 1 (ensure
currentCount is passed or tracked), keep the 300ms delay for realism, and return
the updated { topicId, liked, newCount } so optimistic updates behave
predictably.

In `@src/features/topics/topics.types.ts`:
- Around line 110-111: The JSDoc for the optional pageSize field is wrong:
update the comment for pageSize in topics.types.ts (and the duplicate JSDoc at
the second occurrence around line 127) to reflect the real default used in code
(PAGE_SIZES.TOPICS which is 5) instead of "기본값: 10"; ensure both JSDoc lines
mention "기본값: 5" to match the implementation in topics.api.ts.
🧹 Nitpick comments (14)
src/shared/ui/LikeButton.tsx (1)

52-57: isPending 상태에서 시각적 피드백이 없습니다.

disabled일 때는 border-transparent opacity-100이 적용되지만, isPending만 true인 경우 별도 시각적 변화가 없어 사용자가 버튼이 비활성 상태임을 인지하기 어렵습니다.

Optimistic update 패턴에서 의도된 동작이라면 무시해도 됩니다. 만약 피드백이 필요하다면 isPendingopacity-70이나 cursor-wait 등을 고려해 보세요.

src/features/topics/hooks/useInfiniteScroll.ts (1)

34-34: onLoadMore가 useEffect 의존성 배열에 포함되어 있어 불필요한 observer 재생성 가능성이 있습니다.

호출부에서 인라인 함수로 onLoadMore를 전달하면 매 렌더마다 observer가 disconnect/reconnect됩니다. useCallback으로 감싸거나, 내부에서 useRef로 최신 콜백을 저장하는 패턴을 사용하세요.

제안: ref로 최신 콜백 안정화
-import { useEffect, useRef } from 'react'
+import { useCallback, useEffect, useRef } from 'react'

 export function useInfiniteScroll({
   hasNextPage,
   isFetchingNextPage,
   onLoadMore,
 }: UseInfiniteScrollOptions) {
   const observerRef = useRef<HTMLDivElement>(null)
+  const onLoadMoreRef = useRef(onLoadMore)
+  onLoadMoreRef.current = onLoadMore

   useEffect(() => {
     if (!observerRef.current || !hasNextPage || isFetchingNextPage) return

     const observer = new IntersectionObserver(
       (entries) => {
         if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
-          onLoadMore?.()
+          onLoadMoreRef.current?.()
         }
       },
       { threshold: 0.1 }
     )

     observer.observe(observerRef.current)

     return () => observer.disconnect()
-  }, [hasNextPage, isFetchingNextPage, onLoadMore])
+  }, [hasNextPage, isFetchingNextPage])

   return observerRef
 }
src/features/topics/topics.mock.ts (1)

401-442: Mock 함수의 기본 pageSize(10)가 API 레이어의 기본값(5)과 다릅니다.

getMockProposedTopics(pageSize = 10)getMockConfirmedTopics(pageSize = 10)의 기본값이 PAGE_SIZES.TOPICS(5)와 불일치합니다. 현재 API 레이어에서 항상 pageSize를 넘기므로 런타임 영향은 없지만, 직접 호출 시 혼동 가능성이 있습니다.

제안: 기본값 일치
 export const getMockProposedTopics = (
-  pageSize: number = 10,
+  pageSize: number = 5,
   cursorLikeCount?: number,
   cursorTopicId?: number
 ): GetProposedTopicsResponse => {

getMockConfirmedTopics에도 동일하게 적용하세요.

src/features/topics/topics.types.ts (1)

31-82: ProposedTopicItemConfirmedTopicItem의 공통 필드가 많습니다.

topicId, title, description, topicType, topicTypeLabel, likeCount, createdByInfo 등 7개 필드가 중복됩니다. 향후 필드 변경 시 양쪽을 모두 수정해야 하므로, 공통 base type 추출을 고려해 볼 수 있습니다.

현재 규모에서는 급하지 않으나, 타입이 더 늘어날 경우 유지보수에 도움이 됩니다.

예시: 공통 base type 추출
type TopicItemBase = {
  topicId: number
  title: string
  description: string
  topicType: TopicType
  topicTypeLabel: string
  likeCount: number
  createdByInfo: {
    userId: number
    nickname: string
  }
}

export type ProposedTopicItem = TopicItemBase & {
  meetingId: number
  topicStatus: TopicStatus
  isLiked: boolean
  canDelete: boolean
}

export type ConfirmedTopicItem = TopicItemBase & {
  confirmOrder: number
}
src/features/topics/components/DefaultTopicCard.tsx (1)

13-15: justify-between이 자식 요소 하나에만 적용되어 효과가 없습니다.

현재 <div className="flex justify-between items-end"> 안에 <p> 하나만 있어 justify-between이 의미 없습니다. 향후 오른쪽에 요소가 추가될 예정이라면 무시해도 됩니다.

src/features/topics/components/TopicHeader.tsx (1)

79-79: Check 아이콘의 size가 문자열로 전달되고 있습니다.

lucide-reactsize prop은 number | string이지만 숫자가 관례적입니다.

-<Check size="20" />
+<Check size={20} />
src/features/topics/hooks/useLikeTopic.ts (1)

70-72: likeCount가 0일 때 unlike하면 음수가 됩니다.

데이터 불일치 시 likeCount - 1이 음수가 될 수 있습니다. 방어 코드를 추가하면 안전합니다.

-likeCount: newIsLiked ? topic.likeCount + 1 : topic.likeCount - 1,
+likeCount: newIsLiked ? topic.likeCount + 1 : Math.max(0, topic.likeCount - 1),
src/features/topics/hooks/useConfirmedTopics.ts (1)

42-50: topicQueryKeys를 사용하지 않고 인라인 queryKey를 정의하고 있습니다.

topicQueryKeys 모듈이 배럴에서 export되고 있지만 여기서는 사용하지 않습니다. useDeleteTopic, useLikeTopic 등 mutation 훅에서도 동일한 키를 인라인으로 작성하고 있어, 키 불일치 시 캐시 무효화/낙관적 업데이트가 조용히 실패할 수 있습니다.

중앙 관리 키를 일관되게 사용하면 이런 위험을 제거할 수 있습니다.

#!/bin/bash
# topicQueryKeys가 어떻게 정의되어 있는지, 실제로 사용되는 곳이 있는지 확인
fd topicQueryKeys --type f --exec cat {}
echo "---"
rg -n "topicQueryKeys" --type ts

As per coding guidelines: "queryKey 안정성, enabled 조건, select 비용, invalidate/refetch 타이밍을 중점적으로 봐줘."

src/features/topics/components/TopicCard.tsx (1)

6-18: gatheringId, meetingId, topicId가 optional이면 mutation 호출 시점에 매번 방어 코드가 필요합니다.

현재 handleDeletehandleLike에서 각각 falsy 체크를 하고 있어 동작은 정상이지만, 이 세 값이 없으면 카드가 사실상 상호작용 불가 상태입니다. 호출부에서 required로 보장하거나, 별도의 "읽기 전용" variant를 분리하는 것도 고려할 수 있습니다.

src/features/topics/index.ts (1)

13-13: topics.mock를 feature 배럴에서 re-export하고 있습니다.

topics.api.ts에서 이미 mock 함수를 직접 import하고 있어 번들 포함은 불가피하지만, 배럴에서 public으로 export하면 다른 feature에서 실수로 mock을 가져다 쓸 수 있습니다. mock 제거 시점에 배럴에서도 함께 제거하는 것을 잊지 마세요.

src/pages/Meetings/MeetingDetailPage.tsx (3)

32-56: 두 탭의 무한 쿼리가 동시에 실행됩니다.

activeTab 상태와 무관하게 useProposedTopicsuseConfirmedTopics가 컴포넌트 마운트 시 동시에 호출됩니다. 비활성 탭 데이터까지 즉시 fetch하면 불필요한 네트워크 요청이 발생합니다.

각 훅의 호출부에서 enabled 조건으로 활성 탭을 추가하면 비활성 탭의 초기 요청을 지연시킬 수 있습니다. 예를 들어 useProposedTopicsenabled 옵션을 전달하거나, 훅 내부에서 추가 조건을 받을 수 있도록 확장하는 방안을 고려해 보세요.


59-66: 에러 처리에 alert() 사용은 UX 측면에서 개선 여지가 있습니다.

alert()는 브라우저 메인 스레드를 블로킹하고, 두 에러가 동시에 발생하면 alert가 연속으로 표시됩니다. Toast/Snackbar 등 비차단형 알림으로 전환하면 사용자 경험이 개선됩니다. 당장은 아니더라도 추후 개선 항목으로 남겨두시면 좋겠습니다.


138-163: TabsContent 내부 구조가 거의 동일합니다 — 공통 추출 가능성 검토.

두 탭(PROPOSED, CONFIRMED)의 렌더링 패턴(로딩 → TopicHeader → TopicList)이 거의 동일합니다. 공통 래퍼 컴포넌트나 헬퍼 함수로 추출하면 반복을 줄이고 유지보수가 쉬워집니다. 당장 필수는 아니지만 탭이 추가되거나 로직이 변경될 때 유리합니다.

Also applies to: 166-190

src/features/topics/components/ConfirmedTopicList.tsx (1)

16-81: ProposedTopicList와 구조가 거의 동일합니다 — 공통 추출 고려.

가상화 컨테이너, 스켈레톤, 무한 스크롤 트리거 로직이 ProposedTopicList와 사실상 동일합니다. 공통 VirtualTopicList 베이스 컴포넌트로 추출하고 차이점(DefaultTopicCard, EmptyTopicList, TopicCard props)만 주입하면 중복을 크게 줄일 수 있습니다. 급하지는 않으니 후속 리팩터링으로 고려해 주세요.

TanStack Virtual을 사용한 가상화 기능을 제거하고 일반 리스트 렌더링으로 변경.
무한 스크롤 훅은 shared/hooks로 이동하여 재사용성 향상.
좋아요 낙관적 업데이트 로직을 개선하여 모든 관련 쿼리에 일관되게 적용.
Query Key 관리를 topicQueryKeys로 통일하여 유지보수성 향상.
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/features/topics/components/ConfirmedTopicList.tsx`:
- Around line 29-31: ConfirmedTopicList currently early-returns <EmptyTopicList
/> when topics.length === 0 which prevents the infinite-scroll sentinel from
mounting in the edge case of first-page [] + hasNext: true; instead, don't early
return — render EmptyTopicList as part of the normal tree and always render the
sentinel element that uses the ref from useInfiniteScroll (or attach the hook's
ref into EmptyTopicList) so the sentinel mounts even for empty topics; update
ConfirmedTopicList to render the sentinel div with the hook's ref (from
useInfiniteScroll) alongside or inside EmptyTopicList and keep the existing
hasNext logic intact.
🧹 Nitpick comments (2)
src/features/topics/hooks/useDeleteTopic.ts (1)

41-41: mutationFn 래핑 함수 제거 가능

(params: DeleteTopicParams) => deleteTopic(params)deleteTopic로 직접 전달하면 불필요한 래핑을 줄일 수 있습니다.

♻️ 수정 제안
  return useMutation<void, ApiError, DeleteTopicParams>({
-   mutationFn: (params: DeleteTopicParams) => deleteTopic(params),
+   mutationFn: deleteTopic,
    onSuccess: (_, variables) => {
src/features/topics/hooks/useLikeTopic.ts (1)

59-62: data! 비-널 단언이 .filter() 전에 적용되어 타입 안전성이 깨집니다.

getQueriesDatadataundefined일 수 있습니다. 현재 코드는 data!로 먼저 단언한 뒤 .filter()로 걸러내는데, TypeScript 관점에서 이미 non-null로 캐스팅되어 필터의 타입 가드 효과가 사라집니다.

→ 런타임에서는 동작하지만, .filter()를 먼저 적용하면 타입도 안전해집니다.

♻️ filter-first 방식으로 변경
       const previousQueries = queryClient
         .getQueriesData<InfiniteData<GetProposedTopicsResponse>>({ queryKey: baseQueryKey })
-        .map(([queryKey, data]) => ({ queryKey, data: data! }))
-        .filter((query) => query.data !== undefined)
+        .filter((entry): entry is [readonly unknown[], InfiniteData<GetProposedTopicsResponse>] => entry[1] !== undefined)
+        .map(([queryKey, data]) => ({ queryKey, data }))

Copy link
Contributor

@mgYang53 mgYang53 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 👍

@haruyam15 haruyam15 merged commit bd82478 into develop Feb 7, 2026
2 checks passed
@haruyam15 haruyam15 deleted the feat/topics-list-46 branch February 8, 2026 14:31
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] 주제 조회 UI 및 기능 개발

3 participants