-
Notifications
You must be signed in to change notification settings - Fork 0
feat: dashboard k6 - 대시보드 리펙토링 버전 #1
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,11 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query | |
|
|
||
| import DashBoardSummary from '@/features/dashboard/components/DashboardSummary'; | ||
| import DashboardDetail from '@/features/dashboard/components/DashboardDetail'; | ||
| import DashboardDetailSkeleton from '@/features/dashboard/components/DashboardDetailSkeleton'; | ||
|
|
||
| import { todoQueries, userQueries, goalQueries } from '@/shared/lib/query/queryKeys'; | ||
| import { dashboardKeys } from '@/shared/lib/query/keyFactory'; | ||
| import { getDashboardSummary } from '@/features/dashboard/lib/getDashboardSummary'; | ||
| import { DataBoundary } from '@/shared/components/ErrorSuspenseBoundary'; | ||
|
|
||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
|
|
@@ -15,37 +18,22 @@ export const dynamic = 'force-dynamic'; | |
| export default async function DashboardPage() { | ||
| const queryClient = new QueryClient(); | ||
|
|
||
| await Promise.all([ | ||
| queryClient.prefetchQuery(userQueries.current()), | ||
| queryClient.prefetchQuery(userQueries.progress()), | ||
| ]); | ||
|
|
||
| const goals = await queryClient.fetchQuery(goalQueries.list()); | ||
|
|
||
| await Promise.all( | ||
| (goals.goals ?? []) | ||
| .filter((goal) => goal.id != null) | ||
| .flatMap((goal) => [ | ||
| queryClient.prefetchQuery(goalQueries.detail(goal.id!)), | ||
| queryClient.prefetchInfiniteQuery({ | ||
| ...todoQueries.infiniteList({ goalId: goal.id!, done: false, limit: 10 }), | ||
| pages: 1, | ||
| }), | ||
| queryClient.prefetchInfiniteQuery({ | ||
| ...todoQueries.infiniteList({ goalId: goal.id!, done: true, limit: 10 }), | ||
| pages: 1, | ||
| }), | ||
| ]), | ||
| ); | ||
| await queryClient.prefetchQuery({ | ||
| queryKey: dashboardKeys.summary(), | ||
| queryFn: async () => (await getDashboardSummary()).data, | ||
| }); | ||
|
|
||
| const dehydratedState = dehydrate(queryClient); | ||
|
|
||
| return ( | ||
| <HydrationBoundary state={dehydratedState}> | ||
| <div className="flex w-full flex-col"> | ||
| <div className="flex w-full flex-col"> | ||
| <HydrationBoundary state={dehydratedState}> | ||
| <DashBoardSummary /> | ||
| </HydrationBoundary> | ||
|
|
||
| <DataBoundary suspenseFallback={<DashboardDetailSkeleton />}> | ||
| <DashboardDetail /> | ||
| </div> | ||
| </HydrationBoundary> | ||
| </DataBoundary> | ||
|
Comment on lines
+34
to
+36
|
||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { fetchDashboard } from '@/shared/lib/api/fetchDashboard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await fetchDashboard.getDashboardSummaryResult(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json(result, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: result.hasAnySuccess ? 200 : 502, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Cache-Control': 'no-store', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ message: 'Failed to fetch dashboard summary' }, { status: 502 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+16
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET() { | |
| try { | |
| const result = await fetchDashboard.getDashboardSummaryResult(); | |
| return NextResponse.json(result, { | |
| status: result.hasAnySuccess ? 200 : 502, | |
| headers: { | |
| 'Cache-Control': 'no-store', | |
| }, | |
| }); | |
| } catch { | |
| return NextResponse.json({ message: 'Failed to fetch dashboard summary' }, { status: 502 }); | |
| function findAuthStatus(value: unknown, seen = new WeakSet<object>()): 401 | 403 | undefined { | |
| if (!value || (typeof value !== 'object' && typeof value !== 'function')) { | |
| return undefined; | |
| } | |
| const candidate = value as { | |
| status?: unknown; | |
| statusCode?: unknown; | |
| response?: unknown; | |
| cause?: unknown; | |
| error?: unknown; | |
| errors?: unknown; | |
| }; | |
| if (candidate.status === 401 || candidate.statusCode === 401) { | |
| return 401; | |
| } | |
| if (candidate.status === 403 || candidate.statusCode === 403) { | |
| return 403; | |
| } | |
| if (seen.has(value as object)) { | |
| return undefined; | |
| } | |
| seen.add(value as object); | |
| const nestedValues = [ | |
| candidate.response, | |
| candidate.cause, | |
| candidate.error, | |
| candidate.errors, | |
| ...Object.values(candidate), | |
| ]; | |
| for (const nestedValue of nestedValues) { | |
| const authStatus = findAuthStatus(nestedValue, seen); | |
| if (authStatus) { | |
| return authStatus; | |
| } | |
| } | |
| return undefined; | |
| } | |
| export async function GET() { | |
| try { | |
| const result = await fetchDashboard.getDashboardSummaryResult(); | |
| const authStatus = !result.hasAnySuccess ? findAuthStatus(result) : undefined; | |
| return NextResponse.json(result, { | |
| status: authStatus ?? (result.hasAnySuccess ? 200 : 502), | |
| headers: { | |
| 'Cache-Control': 'no-store', | |
| }, | |
| }); | |
| } catch (error) { | |
| const authStatus = findAuthStatus(error); | |
| return NextResponse.json( | |
| { message: 'Failed to fetch dashboard summary' }, | |
| { status: authStatus ?? 502 }, | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,25 +14,26 @@ import TaskCardWrapper from '../TaskCardWrapper'; | |
| import GithubRepoConnectModal from '@/shared/components/Modal/GithubRepoConnectModal'; | ||
|
|
||
| import { useBreakpoint } from '@/shared/hooks/useBreakPoint'; | ||
|
|
||
| import { todoQueries, userQueries, goalQueries } from '@/shared/lib/query/queryKeys'; | ||
| import { dashboardQueries } from '@/shared/lib/query/queryKeys'; | ||
| import { useModalStore } from '@/shared/stores/useModalStore'; | ||
| import { useTodoModeStore, TodoMode } from '@/shared/stores/useTodoModeStore'; | ||
| import { useLanguage } from '@/shared/contexts/LanguageContext'; | ||
| import { GITHUB_DISCONNECTED_SESSION_KEY } from '@/shared/constants/github'; | ||
| import { DashboardSummaryResponse } from '@/shared/types/api/schemas/api.process'; | ||
|
|
||
| export default function DashBoardSummary() { | ||
| const { t } = useLanguage(); | ||
|
|
||
| const { data: user, isFetched: isUserFetched } = useQuery(userQueries.current()); | ||
| const { data: goals, isFetched: isGoalsFetched } = useQuery(goalQueries.list()); | ||
| const breakpoint = useBreakpoint(); | ||
|
|
||
| const mode = useTodoModeStore((state) => state.mode); | ||
| const setMode = useTodoModeStore((state) => state.setMode); | ||
|
|
||
| const { data: dashboardSummaryData, isFetched: isDashboardSummaryFetched } = useQuery({ | ||
| ...dashboardQueries.summary(), | ||
| }); | ||
|
ramong26 marked this conversation as resolved.
|
||
|
|
||
| const { openModal } = useModalStore(); | ||
|
|
||
| const githubGoals = goals?.goals?.filter((goal) => goal.source === 'GITHUB') ?? []; | ||
| const githubGoals = dashboardSummaryData?.todos?.filter((goal) => goal.source === 'GITHUB') ?? []; | ||
| const isGithubDisconnectedSession = | ||
| typeof window !== 'undefined' && window.sessionStorage.getItem(GITHUB_DISCONNECTED_SESSION_KEY) === 'true'; | ||
|
Comment on lines
+36
to
38
|
||
|
|
||
|
|
@@ -48,14 +49,14 @@ export default function DashBoardSummary() { | |
| return; | ||
| } | ||
|
|
||
| if (!isUserFetched) { | ||
| if (!isDashboardSummaryFetched) { | ||
| return; | ||
| } | ||
|
|
||
| const shouldOpenConnectModal = | ||
| isGithubDisconnectedSession || | ||
| !user?.githubConnected || | ||
| (user.githubConnected && isGoalsFetched && githubGoals.length === 0); | ||
| !dashboardSummaryData?.user?.githubConnected || | ||
| (dashboardSummaryData?.user?.githubConnected && isDashboardSummaryFetched && githubGoals.length === 0); | ||
|
|
||
| if (!shouldOpenConnectModal) { | ||
| githubModalOpenedRef.current = false; | ||
|
|
@@ -68,11 +69,10 @@ export default function DashBoardSummary() { | |
| } | ||
| }, [ | ||
| mode, | ||
| isUserFetched, | ||
| isGoalsFetched, | ||
| isDashboardSummaryFetched, | ||
| githubGoals.length, | ||
| openModal, | ||
| user?.githubConnected, | ||
| dashboardSummaryData?.user?.githubConnected, | ||
| isGithubDisconnectedSession, | ||
| ]); | ||
|
|
||
|
|
@@ -81,7 +81,7 @@ export default function DashBoardSummary() { | |
| <div className="flex items-center justify-end pb-[30px] md:justify-between lg:pb-[34px]"> | ||
| {breakpoint !== 'mobile' && ( | ||
| <div className="flex flex-col gap-2"> | ||
| <PageHeader title={`${user?.nickname}${t.dashboard.title}`} /> | ||
| <PageHeader title={`${dashboardSummaryData?.user?.nickname}${t.dashboard.title}`} /> | ||
| {mode === 'GITHUB' && ( | ||
| <span className="text-xl text-gray-400 transition-all duration-200">{t.dashboard.githubModeDesc}</span> | ||
| )} | ||
|
|
@@ -106,35 +106,31 @@ export default function DashBoardSummary() { | |
| </Link> | ||
| } | ||
| /> | ||
|
|
||
| <RecentPostCard /> | ||
| <RecentPostCard dashboardSummaryData={dashboardSummaryData} /> | ||
| </div> | ||
| <div className="flex min-w-0 flex-1 flex-col justify-between gap-[10px]"> | ||
| <PageSubTitle | ||
| subTitle={t.dashboard.myProgress} | ||
| icons={<Image src={'/image/progress-green.png'} alt="Progress Icon" width={40} height={40} />} | ||
| /> | ||
|
|
||
| <CurrentProgressCard /> | ||
| <CurrentProgressCard dashboardSummaryData={dashboardSummaryData} /> | ||
| </div> | ||
| </section> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| function RecentPostCard() { | ||
| interface RecentPostCardProps { | ||
| dashboardSummaryData: DashboardSummaryResponse | undefined; | ||
| } | ||
| function RecentPostCard({ dashboardSummaryData }: RecentPostCardProps) { | ||
| const { t } = useLanguage(); | ||
| const { data: todos } = useQuery( | ||
| todoQueries.list({ | ||
| sort: 'LATEST', | ||
| }), | ||
| ); | ||
| const recentTodos = todos?.todos?.slice(0, 4) ?? []; | ||
|
|
||
| if (!dashboardSummaryData || dashboardSummaryData.todos.length === 0) return null; | ||
| return ( | ||
| <article className="dark:bg-gray-850 flex h-[187px] h-fit w-full min-w-0 flex-col gap-[6px] rounded-[40px] bg-white px-4 py-[18px] md:h-[229px] md:p-4 lg:h-[256px] lg:p-8"> | ||
| {recentTodos.length > 0 ? ( | ||
| recentTodos.map((item) => <TaskCardWrapper key={item.id} item={item} mode="todo" />) | ||
| {dashboardSummaryData?.todos?.length > 0 ? ( | ||
| dashboardSummaryData.todos.map((item) => <TaskCardWrapper key={item.id} item={item} mode="todo" />) | ||
| ) : ( | ||
| <div className="flex h-full items-center justify-center"> | ||
| <span className="text-gray-500">{t.dashboard.noRecentTodo}</span> | ||
|
|
@@ -144,12 +140,14 @@ function RecentPostCard() { | |
| ); | ||
| } | ||
|
|
||
| function CurrentProgressCard() { | ||
| interface CurrentProgressCardProps { | ||
| dashboardSummaryData: DashboardSummaryResponse | undefined; | ||
| } | ||
| function CurrentProgressCard({ dashboardSummaryData }: CurrentProgressCardProps) { | ||
| const { t } = useLanguage(); | ||
| const mode = useTodoModeStore((state) => state.mode); | ||
|
|
||
| const { data: percents } = useQuery(userQueries.progress()); | ||
|
|
||
| if (!dashboardSummaryData || dashboardSummaryData.progress === null) return null; | ||
| return ( | ||
| <article className="bg-bearlog-500 relative h-[187px] w-full rounded-[40px] shadow-[0_10px_40px_0_rgba(2,202,181,0.40)] md:h-[229px] lg:h-[256px]"> | ||
| <div className="absolute right-0 bottom-0"> | ||
|
|
@@ -170,7 +168,11 @@ function CurrentProgressCard() { | |
| </div> | ||
| <div className="absolute flex h-full w-full items-center justify-start gap-8 p-6 lg:p-12"> | ||
| <div className="w-[120px]"> | ||
| <ProgressCircle percent={percents?.totalProgress ?? 0} className="h-auto w-full" color="#008354" /> | ||
| <ProgressCircle | ||
| percent={dashboardSummaryData?.progress?.totalProgress ?? 0} | ||
| className="h-auto w-full" | ||
| color="#008354" | ||
| /> | ||
| </div> | ||
| <div className="flex flex-col items-start gap-2"> | ||
| <div className="flex flex-col items-start"> | ||
|
|
@@ -183,7 +185,7 @@ function CurrentProgressCard() { | |
| </div> | ||
| <div className="flex items-baseline gap-1"> | ||
| <span className="text-[clamp(20px,5vw,60px)] leading-[1] font-bold text-white"> | ||
| {percents?.totalProgress} | ||
| {dashboardSummaryData?.progress?.totalProgress} | ||
| </span> | ||
| <span className="text-[clamp(14px,2vw,30px)] text-white">%</span> | ||
| </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.
goalQueries.list()에 대한 prefetch가 제거되었습니다.DashboardDetail컴포넌트에서 해당 쿼리를 사용하고 있으므로, 서버 사이드에서 미리 데이터를 가져오지 않으면 클라이언트에서 추가적인 네트워크 요청이 발생하여 렌더링 지연(Waterfall)이 생길 수 있습니다. 사용자 경험 향상을 위해dashboardKeys.summary()와 함께 병렬로 prefetch하고,DashboardDetail을HydrationBoundary안으로 포함시키는 것을 권장합니다.