-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor(client): loading error boundary 적용 #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough북마크 페이지를 Suspense + ErrorBoundary 아키텍처로 리팩토링하고, 쿼리 훅을 Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
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 |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (4)
apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx (1)
6-10: 로딩 상태에 에러 이미지와 alt 텍스트 사용 중TODO 주석에서 언급했듯이, 로딩 상태에
chippi_error.svg를 사용하고 있고 alt 텍스트도 "Error"로 되어 있습니다. 접근성과 의미론적 정확성을 위해 로딩 전용 이미지와 적절한 alt 텍스트(예: "Loading")로 변경이 필요합니다.로딩 전용 이미지 에셋이 준비되면 이 이슈를 추적하기 위한 별도 이슈를 생성할까요?
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)
100-101:queryClientprop drilling 개선 가능
queryClient를 prop으로 전달하는 대신,MyBookmarkContent내부에서useQueryClient()를 직접 호출하는 것이 더 깔끔합니다. 이렇게 하면 컴포넌트 간 결합도가 낮아지고 prop 목록이 줄어듭니다.apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)
35-54: 불필요한 쿼리 실행을 방지하세요.세 개의 쿼리가 모두 무조건 실행되지만, 실제로는
activeBadge와category상태에 따라 하나의 데이터만 사용됩니다. 사용하지 않는 쿼리도 Suspense를 트리거하여 불필요한 네트워크 요청과 로딩 상태를 유발할 수 있습니다.조건부로 쿼리를 활성화하는 방식을 고려하세요:
const { data: articlesData, fetchNextPage: fetchNextArticles, hasNextPage: hasNextArticles, -} = useGetBookmarkArticles(); +} = useGetBookmarkArticles({ + enabled: !category && activeBadge === 'all', +}); const { data: unreadArticlesData, fetchNextPage: fetchNextUnreadArticles, hasNextPage: hasNextUnreadArticles, -} = useGetBookmarkUnreadArticles(); +} = useGetBookmarkUnreadArticles({ + enabled: !category && activeBadge === 'notRead', +}); const { data: categoryArticlesData, fetchNextPage: fetchNextCategoryArticles, hasNextPage: hasNextCategoryArticles, } = useGetCategoryBookmarkArticles( categoryId, - activeBadge === 'notRead' ? false : null + activeBadge === 'notRead' ? false : null, + { enabled: !!categoryId } );참고: 이를 위해 쿼리 훅에
enabled옵션을 전달할 수 있도록 수정이 필요합니다.
56-59: null 처리를 더 명확하게 하세요.
useGetCategoryBookmarkArticles는categoryId가 null일 때 쿼리 함수에서null을 반환합니다. 현재 옵셔널 체이닝으로 처리되고 있지만, 타입 안전성을 위해 더 명확한 처리를 권장합니다.const categoryList = - categoryId && categoryArticlesData?.pages - ? categoryArticlesData.pages.flatMap((page) => page.articles) + categoryId && categoryArticlesData?.pages && Array.isArray(categoryArticlesData.pages) + ? categoryArticlesData.pages.flatMap((page) => page?.articles ?? []) : [];
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
apps/client/src/assets/chippi_error.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
apps/client/package.json(1 hunks)apps/client/src/pages/myBookmark/MyBookmark.tsx(3 hunks)apps/client/src/pages/myBookmark/apis/queries.ts(1 hunks)apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx(1 hunks)apps/client/src/shared/components/articlesErrorBoundary/ArticlesErrorBoundary.tsx(1 hunks)apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/client/src/pages/myBookmark/MyBookmark.tsx
🧬 Code graph analysis (2)
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)
apps/client/src/pages/myBookmark/apis/queries.ts (3)
useGetBookmarkArticles(8-16)useGetBookmarkUnreadArticles(18-26)useGetCategoryBookmarkArticles(28-46)apps/client/src/shared/hooks/useInfiniteScroll.ts (1)
useInfiniteScroll(15-51)
apps/client/src/pages/myBookmark/apis/queries.ts (1)
apps/client/src/pages/myBookmark/apis/axios.ts (3)
getBookmarkArticles(3-8)getBookmarkUnreadArticles(10-15)getCategoryBookmarkArticles(17-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (4)
apps/client/package.json (1)
20-20: LGTM!
react-error-boundary라이브러리 추가가 적절합니다. React 19와 호환되는 최신 버전을 사용하고 있으며, Suspense 기반 에러 처리 패턴 구현에 적합한 선택입니다.apps/client/src/pages/myBookmark/MyBookmark.tsx (1)
91-104: Suspense/ErrorBoundary 패턴 적용 승인Suspense와 ErrorBoundary 조합이 올바르게 구성되어 있습니다.
useSuspenseInfiniteQuery를 사용하는MyBookmarkContent가 경계 내부에서 렌더링되어 로딩/에러 상태를 적절히 처리합니다.apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)
87-91: 무한 스크롤 구현이 올바릅니다.
useInfiniteScroll훅의 사용이 적절하며,scrollContainerRef를root로 전달하는 패턴이 올바르게 구현되었습니다.
121-122:scrollContainerRef사용 패턴이 올바르게 구현되어 있습니다.
scrollContainerRef는 부모 컴포넌트(MyBookmark.tsx)에서useRef로 생성되어 (34줄)MyBookmarkContent에 props로 전달되며, 자식 컴포넌트에서 스크롤 컨테이너 요소(121줄)와useInfiniteScroll훅의root옵션(90줄)에 올바르게 사용됩니다. 표준 React 패턴을 따르고 있으므로 추가 조치가 필요하지 않습니다.
| return useSuspenseInfiniteQuery({ | ||
| queryKey: ['categoryBookmarkArticles', readStatus, categoryId], | ||
| queryFn: ({ pageParam = 0 }) => | ||
| getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20), | ||
|
|
||
| queryFn: ({ pageParam = 0 }) => { | ||
| if (!categoryId) return null; | ||
| return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20); | ||
| }, | ||
|
|
||
| initialPageParam: 0, | ||
| getNextPageParam: (lastPage, allPages) => { | ||
| if (lastPage.articles.length === 0) { | ||
| return undefined; | ||
| } | ||
| if (!lastPage || lastPage.articles.length === 0) return undefined; | ||
| return allPages.length; | ||
| }, | ||
| enabled: !!categoryId, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useSuspenseInfiniteQuery에서 categoryId 부재 시 skipToken 사용
TypeScript 사용자는 enabled: false의 대안으로 skipToken을 사용할 수 있으며, 이는 조건에 따라 쿼리를 비활성화하면서도 타입 안전성을 유지하는 데 유용합니다. TanStack Query v5에서 useSuspenseInfiniteQuery는 skipToken을 queryFn으로 지원합니다.
현재 코드에서 categoryId가 없을 때 null을 반환하는 것보다 queryFn에 조건부 연산자를 사용하여 categoryId ? () => getCategoryBookmarkArticles(...) : skipToken 패턴을 적용하면, 쿼리가 실행되지 않아 더 안전합니다. 이를 통해 getNextPageParam에서 null 값 처리 가드를 제거할 수 있습니다.
+import { useSuspenseInfiniteQuery, skipToken } from '@tanstack/react-query';
export const useGetCategoryBookmarkArticles = (
categoryId: string | null,
readStatus: boolean | null
) => {
return useSuspenseInfiniteQuery({
queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
- queryFn: ({ pageParam = 0 }) => {
- if (!categoryId) return null;
- return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20);
- },
+ queryFn: categoryId
+ ? ({ pageParam = 0 }) =>
+ getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20)
+ : skipToken,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
- if (!lastPage || lastPage.articles.length === 0) return undefined;
+ if (lastPage.articles.length === 0) return undefined;
return allPages.length;
},
});
};📝 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.
| return useSuspenseInfiniteQuery({ | |
| queryKey: ['categoryBookmarkArticles', readStatus, categoryId], | |
| queryFn: ({ pageParam = 0 }) => | |
| getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20), | |
| queryFn: ({ pageParam = 0 }) => { | |
| if (!categoryId) return null; | |
| return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20); | |
| }, | |
| initialPageParam: 0, | |
| getNextPageParam: (lastPage, allPages) => { | |
| if (lastPage.articles.length === 0) { | |
| return undefined; | |
| } | |
| if (!lastPage || lastPage.articles.length === 0) return undefined; | |
| return allPages.length; | |
| }, | |
| enabled: !!categoryId, | |
| }); | |
| return useSuspenseInfiniteQuery({ | |
| queryKey: ['categoryBookmarkArticles', readStatus, categoryId], | |
| queryFn: categoryId | |
| ? ({ pageParam = 0 }) => | |
| getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20) | |
| : skipToken, | |
| initialPageParam: 0, | |
| getNextPageParam: (lastPage, allPages) => { | |
| if (lastPage.articles.length === 0) return undefined; | |
| return allPages.length; | |
| }, | |
| }); |
| onBadgeChange: (type: 'all' | 'notRead') => void; | ||
| category: string | null; | ||
| categoryId: string | null; | ||
| updateToReadStatus: (id: number, options?: any) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
타입 안전성을 위해 any 타입을 구체적인 타입으로 교체하세요.
Line 19의 options?: any와 Line 21의 queryClient: any는 타입 안전성을 저하시킵니다. QueryClient 타입(from @tanstack/react-query)과 mutation 옵션의 구체적인 타입을 사용하세요.
다음 diff를 적용하여 타입을 개선하세요:
+import { QueryClient } from '@tanstack/react-query';
+
interface MyBookmarkContentProps {
activeBadge: 'all' | 'notRead';
onBadgeChange: (type: 'all' | 'notRead') => void;
category: string | null;
categoryId: string | null;
- updateToReadStatus: (id: number, options?: any) => void;
+ updateToReadStatus: (id: number, options?: { onSuccess?: () => void; onError?: (error: Error) => void }) => void;
openMenu: (id: number, anchor: HTMLElement) => void;
- queryClient: any;
+ queryClient: QueryClient;
scrollContainerRef: MutableRefObject<HTMLDivElement | null>;
}Also applies to: 21-21
🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 19 and 21, replace the loose any types with react-query types:
change options?: any to a concrete UseMutationOptions type (e.g.
UseMutationOptions<void, unknown, number, unknown> or another appropriate
generics matching your mutation result/error/variables/context) and change
queryClient: any to queryClient: QueryClient from @tanstack/react-query; import
QueryClient and UseMutationOptions at the top and update the function signature
to use these types so the mutation options and query client are type-safe.
| /** Empty 상태 컴포넌트 */ | ||
| const EmptyStateComponent = () => { | ||
| if (articlesToDisplay.length === 0) { | ||
| if (articlesData?.pages?.[0]?.totalArticle === 0) return <NoArticles />; | ||
| return <NoUnreadArticles />; | ||
| } | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty 상태 판단 로직이 잘못되었습니다.
Line 96에서 articlesData?.pages?.[0]?.totalArticle === 0을 확인하지만, articlesToDisplay는 activeBadge나 category에 따라 다른 데이터 소스(unreadArticlesData 또는 categoryArticlesData)에서 올 수 있습니다. 이로 인해 잘못된 Empty 상태가 표시될 수 있습니다.
다음과 같이 수정하세요:
const EmptyStateComponent = () => {
if (articlesToDisplay.length === 0) {
- if (articlesData?.pages?.[0]?.totalArticle === 0) return <NoArticles />;
+ if (totalArticle === 0) return <NoArticles />;
return <NoUnreadArticles />;
}
return null;
};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 93 to 100, the EmptyStateComponent checks
articlesData?.pages?.[0]?.totalArticle which is wrong because articlesToDisplay
can come from unreadArticlesData or categoryArticlesData depending on
activeBadge/category; update the logic to inspect the correct source: if
activeBadge indicates "unread" use unreadArticlesData?.pages?.[0]?.totalArticle,
else if a category filter is active use
categoryArticlesData?.pages?.[0]?.totalArticle, otherwise fall back to
articlesData?.pages?.[0]?.totalArticle; return NoArticles when the chosen
source's totalArticle === 0, return NoUnreadArticles only when the unread source
is selected and its list is empty, otherwise return null.
| onClick={() => { | ||
| window.open(article.url, '_blank'); | ||
| updateToReadStatus(article.articleId, { | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ['bookmarkReadArticles'], | ||
| }); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ['bookmarkUnreadArticles'], | ||
| }); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ['categoryBookmarkArticles'], | ||
| }); | ||
| queryClient.invalidateQueries({ queryKey: ['arcons'] }); | ||
| }, | ||
| onError: (error: any) => { | ||
| console.error(error); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
쿼리 무효화 로직을 개선하고 에러 처리를 정리하세요.
현재 구현에는 몇 가지 개선 가능한 부분이 있습니다:
- 하드코딩된 쿼리 키: Lines 133, 136, 139, 141의 쿼리 키가 하드코딩되어 있어
queries.ts의 실제 쿼리 키와 불일치할 위험이 있습니다. - 프로덕션 환경의 console.error: Line 144의
console.error는 프로덕션 환경에서 적절하지 않을 수 있습니다. - 에러 토스트/알림 누락: 사용자에게 오류를 알리는 UI 피드백이 없습니다.
다음과 같이 개선하세요:
+// queries.ts에서 쿼리 키를 export
+export const QUERY_KEYS = {
+ bookmarkReadArticles: ['bookmarkReadArticles'],
+ bookmarkUnreadArticles: ['bookmarkUnreadArticles'],
+ categoryBookmarkArticles: ['categoryBookmarkArticles'],
+ arcons: ['arcons'],
+} as const; onClick={() => {
window.open(article.url, '_blank');
updateToReadStatus(article.articleId, {
onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: ['bookmarkReadArticles'],
- });
- queryClient.invalidateQueries({
- queryKey: ['bookmarkUnreadArticles'],
- });
- queryClient.invalidateQueries({
- queryKey: ['categoryBookmarkArticles'],
- });
- queryClient.invalidateQueries({ queryKey: ['arcons'] });
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.bookmarkReadArticles,
+ });
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.bookmarkUnreadArticles,
+ });
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.categoryBookmarkArticles,
+ });
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.arcons
+ });
},
onError: (error: any) => {
- console.error(error);
+ // TODO: 사용자에게 에러 토스트 표시
+ if (process.env.NODE_ENV === 'development') {
+ console.error('Failed to update read status:', error);
+ }
},
});
}}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 128 to 146, replace the hardcoded invalidateQueries keys with the
centralized query key constants exported from queries.ts (import the keys at top
and call queryClient.invalidateQueries with those constants) and remove the
console.error; instead surface errors to users via the app's toast/notification
helper (or logger) by calling the existing showErrorToast or toast.error with a
user-friendly message plus optional error details, and ensure success/error
handlers consistently invalidate the relevant query constants so UI state
updates reliably.
| const ArticlesErrorBoundary = () => { | ||
| return ( | ||
| <div className="mt-[10rem] flex flex-col items-center justify-center text-center"> | ||
| <h1 className="text-main500 head1 mb-[1rem]">404 ERROR</h1> | ||
|
|
||
| <p className="body1-m text-font-gray-3 mb-[3rem]"> | ||
| 죄송합니다. 페이지를 찾을 수 없습니다. | ||
| </p> | ||
|
|
||
| <img | ||
| src={chippiError} | ||
| alt="Error" | ||
| className="mt-[1rem] h-auto w-[18rem]" | ||
| /> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 폴백 컴포넌트에서 에러 복구 기능 누락
ErrorBoundary의 FallbackComponent는 error와 resetErrorBoundary props를 받습니다. 현재 구현에서는 이를 활용하지 않아 사용자가 에러 상태에서 복구할 수 없습니다.
또한 "404 ERROR"는 HTTP 상태 코드를 의미하는데, 이 컴포넌트는 JavaScript 런타임 에러나 쿼리 실패를 처리하므로 메시지가 혼란을 줄 수 있습니다.
+import { FallbackProps } from 'react-error-boundary';
import chippiError from '@assets/chippi_error.svg';
-const ArticlesErrorBoundary = () => {
+const ArticlesErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
return (
<div className="mt-[10rem] flex flex-col items-center justify-center text-center">
- <h1 className="text-main500 head1 mb-[1rem]">404 ERROR</h1>
+ <h1 className="text-main500 head1 mb-[1rem]">오류 발생</h1>
<p className="body1-m text-font-gray-3 mb-[3rem]">
- 죄송합니다. 페이지를 찾을 수 없습니다.
+ 죄송합니다. 데이터를 불러오는 중 문제가 발생했습니다.
</p>
<img
src={chippiError}
alt="Error"
className="mt-[1rem] h-auto w-[18rem]"
/>
+
+ <button
+ onClick={resetErrorBoundary}
+ className="mt-[2rem] rounded-lg bg-main500 px-[2rem] py-[1rem] text-white"
+ >
+ 다시 시도
+ </button>
</div>
);
};🤖 Prompt for AI Agents
In
apps/client/src/shared/components/articlesErrorBoundary/ArticlesErrorBoundary.tsx
around lines 3 to 18, the component currently ignores the ErrorBoundary
FallbackComponent props and shows a misleading "404 ERROR" message; update the
component signature to accept the standard FallbackComponent props (error and
resetErrorBoundary), replace the "404 ERROR" heading with a generic runtime
error message (e.g., "Something went wrong"), optionally render a sanitized
error.message for debugging, and add a visible "Retry" button that calls
resetErrorBoundary when clicked so users can recover from the error state.
into refactor/#208/loading-error-boundary
jllee000
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suspense관련 적어주신 아티클로 공부 많이 됐습니다! 로딩 바운더리의 신...! 고생하셨습니다
| return useSuspenseInfiniteQuery({ | ||
| queryKey: ['bookmarkReadArticles'], | ||
| queryFn: ({ pageParam = 0 }) => getBookmarkArticles(pageParam, 20), | ||
| initialPageParam: 0, | ||
| getNextPageParam: (lastPage, allPages) => { | ||
| if (lastPage.articles.length === 0) { | ||
| return undefined; | ||
| } | ||
| return allPages.length; | ||
| }, | ||
| getNextPageParam: (lastPage, allPages) => | ||
| lastPage.articles.length === 0 ? undefined : allPages.length, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suspenseInfiniteQuery는 진심으로 처음 봤는데,, tanstack이 지원해주는게 참 많구만요
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
처음해보는거라 공부할 것도 많고 문제도 많았어요 그중에 이슈 있었던 부분 아티클로 정리해둬서 글 링크로 첨부합니다
만약 Suspense 바깥에서 동일한 API를 불러온다면?
📷 Screenshot
2025-12-17.4.55.35.mov
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.