Conversation
Walkthrough페이지별 메타데이터 생성 함수가 추가/갱신되고, 여러 경로의 Open Graph 이미지 URL이 절대 HTTPS로 변경되었습니다. 또한 프로젝트 목록 UI가 기존 복잡한 클라이언트 페이지에서 새로운 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Client as ProjectListPage
participant API
participant Router
rect rgba(0,128,255,0.5)
User->>Client: 페이지 접근
Client->>API: 초기 데이터 요청 (useRecruitingPosts)
API-->>Client: 초기 데이터 응답
Client->>Client: 데스크탑/모바일 렌더링 분기
end
rect rgba(0,200,100,0.5)
Note over Client,User: 모바일 스크롤에 따른 추가 로드
User->>Client: 스크롤 → 관찰 영역 진입
Client->>Client: intersection observer 트리거
Client->>API: 추가 페이지 요청 (useInfiniteRecruitingPosts)
API-->>Client: 추가 데이터 응답
Client->>Client: displayCount 업데이트 및 렌더링
end
Router-->>Client: 카드 클릭 → 상세 페이지 네비게이션
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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/app/`(main)/project/page.tsx:
- Around line 20-38: generateMetadata is a server-only export but your page
currently exposes client-side code ('use client'); split the page into a server
page that exports generateMetadata and renders a separate client component for
interactive logic. Move client logic into a new component (e.g.,
ProjectPageContent) without exporting generateMetadata from it, keep
generateMetadata in page.tsx (server component) and import/render
ProjectPageContent there, and remove any 'use client' directive from the file
that defines generateMetadata so the server API is used correctly.
🧹 Nitpick comments (1)
src/app/(main)/teampsylog/head/[uuid]/page.tsx (1)
12-46: 중복 코드 리팩토링을 고려해보세요.
generateMetadata함수가question/[uuid]/page.tsx와 거의 동일합니다. 공통 유틸리티 함수로 추출하면 유지보수성이 향상됩니다.♻️ 예시: 공통 메타데이터 생성 함수
// lib/metadata.ts export async function generateUserMetadata(uuid: string): Promise<Metadata> { let userName = '사용자'; try { const res = await axios.get( `${process.env.NEXT_PUBLIC_API_BASE_URL}/teamficial-log/requester`, { params: { requesterUuid: uuid } }, ); userName = res.data?.result?.requesterName ?? '사용자'; } catch (error) { console.error('Failed to fetch requester data:', error); } return { title: `${userName}님의 팀피셜록을 작성해볼까요?`, description: `소프트스킬 팀빌딩 서비스, 팀피셜`, openGraph: { title: `${userName}님의 팀피셜록을 작성해볼까요?`, description: `소프트스킬 팀빌딩 서비스, 팀피셜`, images: [{ url: 'https://www.teamficial.com/og/Teamficial_metatag_Image.jpg', width: 1200, height: 630 }], }, }; }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/`(main)/project/_components/ProjectList.tsx:
- Around line 35-94: The desktop and mobile query states are being merged
causing global Loading/Error UI; update the hooks useRecruitingPosts and
useInfiniteRecruitingPosts calls to run only when their viewport is active by
adding React Query's enabled flag (e.g., enabled: isDesktopView for
useRecruitingPosts and enabled: isMobileView for useInfiniteRecruitingPosts) so
inactive queries do not fetch or contribute to isLoading/isError; then adjust
isInitialLoading and error checks to consider only the active view's flags (use
isLoading/isError for desktop when isDesktopView, and
isMobileInitialLoading/isMobileError for mobile when isMobileView). Ensure you
reference the existing variables currentPage/pageSize, mobilePageSize, and
displayCount logic unchanged.
🧹 Nitpick comments (1)
src/app/(main)/project/_components/ProjectList.tsx (1)
61-83:inView연속 상태에서 복합적인 페이지 요청 재트리거 시나리오 우려현재 코드는
!isFetchingNextPage가드로 동시 요청을 이미 방지하고 있으나, 다음 흐름에서 재트리거 가능성이 있습니다:
fetchNextPage()완료 →allMobileCards.length증가 → 첫 번째 useEffect 재실행nextDisplayCount > allMobileCards.length조건이 여전히 참이면 추가 요청 발생- 두 번째 useEffect에서
isFetchingNextPage중displayCount클램프를 조건부로 처리하여, 페칭 중에는 첫 번째 effect의 의존성 변경을 최소화하면 흐름이 더 명확합니다.또한
rootMargin을 활용하여 스크롤 끝에서의 빠른 진입/이탈 토글을 줄이는 것도 도움이 됩니다:const { ref, inView } = useInView({ rootMargin: '200px 0px', threshold: 0 })
| const { data, isLoading, isError } = useRecruitingPosts(filters, currentPage, pageSize); | ||
|
|
||
| const { | ||
| data: infiniteData, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| isLoading: isMobileLoading, | ||
| isError: isMobileError, | ||
| isInitialLoading: isMobileInitialLoading, | ||
| } = useInfiniteRecruitingPosts(filters, mobilePageSize); | ||
|
|
||
| const [displayCount, setDisplayCount] = useState(initialDisplayCount); | ||
|
|
||
| useEffect(() => { | ||
| setDisplayCount(initialDisplayCount); | ||
| }, [filters]); | ||
|
|
||
| const { ref, inView } = useInView({ | ||
| threshold: 0, | ||
| }); | ||
|
|
||
| const allMobileCards = infiniteData?.pages.flatMap((page) => page.content) ?? []; | ||
|
|
||
| const mobileCards = allMobileCards.slice(0, displayCount); | ||
|
|
||
| useEffect(() => { | ||
| if (inView && hasNextPage && !isFetchingNextPage) { | ||
| const nextDisplayCount = displayCount + loadMoreCount; | ||
|
|
||
| if (nextDisplayCount > allMobileCards.length) { | ||
| fetchNextPage(); | ||
| } else { | ||
| setDisplayCount(nextDisplayCount); | ||
| } | ||
| } | ||
| }, [inView, hasNextPage, isFetchingNextPage, displayCount, allMobileCards.length, fetchNextPage]); | ||
|
|
||
| useEffect(() => { | ||
| if (infiniteData && allMobileCards.length > 0) { | ||
| const currentTotalCards = allMobileCards.length; | ||
| if (!hasNextPage && displayCount < currentTotalCards) { | ||
| setDisplayCount(currentTotalCards); | ||
| } | ||
| if (displayCount > currentTotalCards) { | ||
| setDisplayCount(Math.max(initialDisplayCount, currentTotalCards)); | ||
| } | ||
| } | ||
| }, [infiniteData, hasNextPage, displayCount, allMobileCards.length]); | ||
|
|
||
| const handleFilterChange = (newFilters: Filters) => { | ||
| setFilters(newFilters); | ||
| setCurrentPage(1); | ||
| }; | ||
|
|
||
| const isInitialLoading = (isLoading && currentPage === 1) || isMobileInitialLoading; | ||
|
|
||
| if (isInitialLoading) return <Loading />; | ||
| if (isError || isMobileError) return <ErrorDisplay />; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
@tanstack/react-query v5 enabled option conditional queries best practices
💡 Result:
Best practices for conditional (“dependent/lazy”) queries in @tanstack/react-query v5
-
Gate the query with
enabledwhen a required input isn’t ready- Use
enabled: !!param(or a boolean function) so the query doesn’t run until the dependency exists (e.g., anid). This is the recommended pattern for dependent/serial queries. [2]
- Use
-
Prefer
skipTokenfor TypeScript-safe conditional query functions- If your query should be disabled when a value is missing, you can keep strong types by returning
skipTokeninstead of a queryFn. [1]
- If your query should be disabled when a value is missing, you can keep strong types by returning
-
If you need manual triggering (
refetch()), preferenabled: falseoverskipTokenrefetch()does not work withskipToken(it will error because there’s no queryFn). If you want a “lazy query” button, useenabled: falseand callrefetch(). [1]
-
Use the right loading flag for disabled/lazy queries
- Disabled queries can be
status: 'pending'while not fetching (fetchStatus: 'idle'), soisPendingcan be misleading. UseisLoading(derived fromisPending && isFetching) to show “first load” spinners. [1]
- Disabled queries can be
-
Avoid request waterfalls when possible
- Dependent queries are inherently a waterfall (slower than parallel). If you can, restructure the API to fetch what you need in one request (or otherwise in parallel). [2]
Common v5 patterns
Dependent query (enabled) [2]
const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail })
const userId = user?.id
const projectsQuery = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
enabled: !!userId,
})TS-safe conditional with skipToken (no manual refetch) [1]
const todosQuery = useQuery({
queryKey: ['todos', filter],
queryFn: filter ? () => fetchTodos(filter) : skipToken,
})Dynamic useQueries: return [] until inputs exist [2]
const usersMessages = useQueries({
queries: userIds
? userIds.map(id => ({ queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id) }))
: [],
})Sources: TanStack Query v5 docs on disabling/pausing queries and skipToken [1], and dependent queries + performance note [2].
🌐 Web query:
React Query v5 useQuery useInfiniteQuery enabled option documentation
💡 Result:
useQuerydocs (v5): Theenabledoption is documented asboolean | (query: Query) => boolean. Setting it tofalsedisables the query from automatically running (useful for dependent/lazy queries). [1]- Disabling/Pausing Queries guide (v5): Explains behavior when
enabled: false(no automatic fetch/refetch; can stillrefetch()manually; ignoresinvalidateQueries/refetchQueriestriggers; initial state differs depending on cached data). [2] useInfiniteQuerydocs:useInfiniteQueryoptions are “identical touseQuery” (so it supportsenabled) plus infinite-specific options likeinitialPageParamandgetNextPageParam. [3]
Sources:
[1] TanStack Query v5 useQuery reference (enabled option) (tanstack.com)
[2] TanStack Query v5 Disabling/Pausing Queries guide (tanstack.dev)
[3] TanStack Query useInfiniteQuery reference (options identical to useQuery) (tanstack.com)
Citations:
- 1: https://tanstack.com/query/v5/docs/react/reference/useQuery?utm_source=openai
- 2: https://tanstack.dev/query/v5/docs/framework/react/guides/disabling-queries?utm_source=openai
- 3: https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery/?utm_source=openai
🌐 Web query:
viewport-based conditional query execution React Query best practices
💡 Result:
Best practices for viewport-based conditional query execution (TanStack/React Query)
-
Never call
useQueryconditionally—control execution withenabled- Keep the hook mounted, and gate network execution with
enabled: inView(or a boolean function). This is the intended “pause/disable” mechanism. [1]
- Keep the hook mounted, and gate network execution with
-
Combine viewport + dependency readiness in
enabled- For queries that also depend on props/state (e.g.,
id), gate on both:enabled: inView && !!id
- This matches the “dependent query” pattern (don’t execute until the dependency exists). [2]
- For queries that also depend on props/state (e.g.,
-
Type-safe conditional execution (TS): use
skipTokenwhen you want to omit thequeryFn- If your query function is only valid when in-view (or when params exist), you can return
skipTokento disable the query in a type-safe way. Note:refetch()won’t work withskipToken, so useenabled: falseif you need manualrefetch. [3]
- If your query function is only valid when in-view (or when params exist), you can return
-
Prefer prefetching before in-view for perceived performance
- If you can predict intent (e.g., the component is near the viewport), prefetch into cache with
queryClient.prefetchQuery(or related helpers). Then, when it becomes visible,useQuerycan render instantly from cache. [4]
- If you can predict intent (e.g., the component is near the viewport), prefetch into cache with
-
For list/pagination/infinite patterns, avoid UI “jumps” while loading
- When the query key changes (page/cursor), use
placeholderData(or thekeepPreviousDatahelper in v5) to keep the previous page’s data visible while fetching the next page. [5]
- When the query key changes (page/cursor), use
Minimal pattern (viewport → enabled)
const { ref, inView } = useInView({ rootMargin: '200px' }) // “near viewport”
const query = useQuery({
queryKey: ['item', id],
queryFn: () => fetchItem(id),
enabled: inView && !!id,
})Sources: Disabling/pausing queries via enabled [1], dependent queries via enabled [2], skipToken behavior and refetch limitation [3], prefetching guidance [4], pagination with placeholderData / keepPreviousData helper [5].
[1] (tanstack.dev)
[2] (tanstack.com)
[3] (tanstack.com)
[4] (tanstack.com)
[5] (tanstack.com)
Citations:
- 1: https://tanstack.dev/query/v5/docs/framework/react/guides/disabling-queries?utm_source=openai
- 2: https://tanstack.com/query/v4/docs/framework/react/guides/dependent-queries?utm_source=openai
- 3: https://tanstack.com/query/latest/docs/react/guides/disabling-queries?utm_source=openai
- 4: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching?utm_source=openai
- 5: https://tanstack.com/query/v5/docs/react/guides/paginated-queries?utm_source=openai
데스크톱/모바일 쿼리 상태를 합산해 로딩·에러가 잘못 노출됨
현재 isInitialLoading/isError가 데스크톱(useRecruitingPosts)과 모바일(useInfiniteRecruitingPosts) 상태를 합산합니다. 모바일 무한 스크롤이 실패하거나 로딩 중이면 데스크톱 화면도 전체가 Loading/Error로 막힙니다.
React Query v5의 enabled 옵션으로 뷰포트 기반 활성 쿼리만 실행하도록 분리하세요. 이는 비활성 쿼리가 불필요하게 fetch되는 것을 방지하고, 상태 병합 문제를 근본적으로 해결합니다.
✅ 제안 수정
+ const [isTabletUp, setIsTabletUp] = useState(false);
+ useEffect(() => {
+ const mq = window.matchMedia('(min-width: 768px)');
+ const sync = () => setIsTabletUp(mq.matches);
+ sync();
+ mq.addEventListener('change', sync);
+ return () => mq.removeEventListener('change', sync);
+ }, []);
+
- const { data, isLoading, isError } = useRecruitingPosts(filters, currentPage, pageSize);
+ const { data, isLoading, isError } = useRecruitingPosts(
+ filters,
+ currentPage,
+ pageSize,
+ { enabled: isTabletUp }
+ );
- const {
- data: infiniteData,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
- isLoading: isMobileLoading,
- isError: isMobileError,
- isInitialLoading: isMobileInitialLoading,
- } = useInfiniteRecruitingPosts(filters, mobilePageSize);
+ const {
+ data: infiniteData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading: isMobileLoading,
+ isError: isMobileError,
+ isInitialLoading: isMobileInitialLoading,
+ } = useInfiniteRecruitingPosts(filters, mobilePageSize, { enabled: !isTabletUp });
- const isInitialLoading = (isLoading && currentPage === 1) || isMobileInitialLoading;
+ const isInitialLoading = isTabletUp
+ ? isLoading && currentPage === 1
+ : isMobileInitialLoading;
if (isInitialLoading) return <Loading />;
- if (isError || isMobileError) return <ErrorDisplay />;
+ if (isTabletUp ? isError : isMobileError) return <ErrorDisplay />;🤖 Prompt for AI Agents
In `@src/app/`(main)/project/_components/ProjectList.tsx around lines 35 - 94, The
desktop and mobile query states are being merged causing global Loading/Error
UI; update the hooks useRecruitingPosts and useInfiniteRecruitingPosts calls to
run only when their viewport is active by adding React Query's enabled flag
(e.g., enabled: isDesktopView for useRecruitingPosts and enabled: isMobileView
for useInfiniteRecruitingPosts) so inactive queries do not fetch or contribute
to isLoading/isError; then adjust isInitialLoading and error checks to consider
only the active view's flags (use isLoading/isError for desktop when
isDesktopView, and isMobileInitialLoading/isMobileError for mobile when
isMobileView). Ensure you reference the existing variables currentPage/pageSize,
mobilePageSize, and displayCount logic unchanged.
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
📸 스크린샷 or 실행영상
🎸 기타 사항 or 추가 코멘트
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.