From 7f4c6b8227c0ed41336e7e9e375ca7a7ada266c6 Mon Sep 17 00:00:00 2001 From: JMJ <89517903+wjsdncl@users.noreply.github.com> Date: Sun, 7 Jul 2024 18:29:12 +0900 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(#167):=20?= =?UTF-8?q?=EB=82=B4=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=84=9C=EB=B2=84=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor(#167): 내 대시보드 페이지 서버 사이드 렌더링 적용 * ♻️ refactor(#167): 내 대시보드 서버 사이드 렌더링 수정 * 🛠 fix(#167): 서버사이드 중복 코드 제거 * 🛠 fix(#167): 초대 목록 함수 수정 * 🛠 fix(#167): 이미지 불러오는 성능 개선 * 🛠 fix(#167): 컨플릭트 해결 --- package.json | 1 + pnpm-lock.yaml | 22 +++++++ .../mydashboard/DashboardList/index.tsx | 56 +++++++++------- .../InvitedDashboardList/index.tsx | 65 ++++++++++--------- src/hooks/useFetchData.ts | 3 +- src/hooks/useSignIn.ts | 2 + src/hooks/useUserDropdown.ts | 2 + src/pages/mydashboard/index.tsx | 48 +++++++++++++- src/utils/createAuthHeaders.ts | 5 ++ 9 files changed, 145 insertions(+), 59 deletions(-) create mode 100644 src/utils/createAuthHeaders.ts diff --git a/package.json b/package.json index a453bf7b..ec2055ac 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tanstack/react-query-devtools": "^5.48.0", "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.7.2", + "cookies-next": "^4.2.1", "js-sha256": "^0.11.0", "lodash": "^4.17.21", "mongoose": "^8.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c0e2e33..40dc89ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: axios: specifier: ^1.7.2 version: 1.7.2 + cookies-next: + specifier: ^4.2.1 + version: 4.2.1 js-sha256: specifier: ^0.11.0 version: 0.11.0 @@ -407,6 +410,9 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -775,6 +781,13 @@ packages: engines: {node: '>=16'} hasBin: true + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cookies-next@4.2.1: + resolution: {integrity: sha512-qsjtZ8TLlxCSX2JphMQNhkm3V3zIMQ05WrLkBKBwu50npBbBfiZWIdmSMzBGcdGKfMK19E0PIitTfRFAdMGHXg==} + cosmiconfig-typescript-loader@5.0.0: resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} @@ -2866,6 +2879,8 @@ snapshots: dependencies: '@types/node': 20.14.5 + '@types/cookie@0.6.0': {} + '@types/hoist-non-react-statics@3.3.5': dependencies: '@types/react': 18.3.3 @@ -3309,6 +3324,13 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + cookie@0.6.0: {} + + cookies-next@4.2.1: + dependencies: + '@types/cookie': 0.6.0 + cookie: 0.6.0 + cosmiconfig-typescript-loader@5.0.0(@types/node@20.14.5)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5): dependencies: '@types/node': 20.14.5 diff --git a/src/containers/mydashboard/DashboardList/index.tsx b/src/containers/mydashboard/DashboardList/index.tsx index 90404fa6..b36c5947 100644 --- a/src/containers/mydashboard/DashboardList/index.tsx +++ b/src/containers/mydashboard/DashboardList/index.tsx @@ -1,6 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Pagination from '@/components/Pagination'; import useFetchData from '@/hooks/useFetchData'; @@ -8,9 +8,14 @@ import useModal from '@/hooks/useModal'; import { getDashboardsList } from '@/services/getService'; import { DashboardsResponse } from '@/types/Dashboard.interface'; -export default function DashboardList() { +interface DashboardListProps { + initialDashboard: DashboardsResponse; +} + +export default function DashboardList({ initialDashboard }: DashboardListProps) { const [currentChunk, setCurrentChunk] = useState(1); const { openNewDashboardModal } = useModal(); + const [dashboardData, setDashboards] = useState(initialDashboard); const { data: dashboardResponse, @@ -20,7 +25,15 @@ export default function DashboardList() { getDashboardsList('pagination', currentChunk, 5), ); - const totalPage = dashboardResponse ? Math.max(1, Math.ceil(dashboardResponse.totalCount / 5)) : 1; + // 총 페이지 수 계산 + const totalPage = dashboardData?.totalCount ? Math.max(1, Math.ceil(dashboardData.totalCount / 5)) : 1; + + // 대시보드 데이터가 변경될 때 상태 업데이트 + useEffect(() => { + if (dashboardResponse) { + setDashboards(dashboardResponse); + } + }, [dashboardResponse]); if (error) { return ( @@ -31,20 +44,26 @@ export default function DashboardList() { ); } + // 다음 페이지로 이동하는 함수 const handleNext = () => { if (currentChunk < totalPage) { setCurrentChunk((prev) => prev + 1); } }; + + // 이전 페이지로 이동하는 함수 const handlePrev = () => { if (currentChunk > 1) { setCurrentChunk((prev) => prev - 1); } }; + if (!dashboardData) return null; + return (
    + {/* 새로운 대시보드 생성 버튼 */}
  • - {isLoading ? ( - <> - {[...Array(5)].map((_, i) => ( + {/* 대시보드 목록 표시 */} + {isLoading && currentChunk !== 1 + ? [...Array(5)].map((_, i) => (
  • - ))} - - ) : ( - <> - {dashboardResponse?.dashboards.map((dashboard) => ( + )) + : dashboardData.dashboards.map((dashboard) => (
  • -

    {dashboard.title}

    +

    {dashboard.title}

    {dashboard.createdByMe && ( my )}
    - arrow - arrow + arrow
  • ))} - - )} - + {/* 페이지네이션 */}
    ([]); +interface InvitedDashboardListProps { + initialInvitedDashboard: InvitationsResponse; +} + +export default function InvitedDashboardList({ initialInvitedDashboard }: InvitedDashboardListProps) { + const [invitations, setInvitations] = useState(initialInvitedDashboard?.invitations || []); const [isFetchingNextPage, setIsFetchingNextPage] = useState(false); const [isSearching, setIsSearching] = useState(false); const observerRef = useRef(null); @@ -26,50 +30,46 @@ export default function InvitedDashboardList() { useEffect(() => { if (data) { setInvitations(data.invitations); - setCursorId(data.cursorId ? data.cursorId : 0); + setCursorId(data.cursorId ?? 0); } }, [data]); - const handleMoreInvitations = async (currentCursorId: number) => { + // 더 많은 초대 데이터를 가져오는 함수 + const handleMoreInvitations = useCallback(async (currentCursorId: number) => { if (currentCursorId !== 0) { + setIsFetchingNextPage(true); try { - setIsFetchingNextPage(true); const { data: nextData } = await getInvitationsList(10, currentCursorId); - if (nextData.invitations.length > 0) { setInvitations((prevInvitations) => [...prevInvitations, ...nextData.invitations]); } - setCursorId(nextData.cursorId || 0); + setCursorId(nextData.cursorId ?? 0); } catch (err) { console.error('데이터를 가져오는 중 오류가 발생했습니다:', err); } finally { setIsFetchingNextPage(false); } } - }; + }, []); + // IntersectionObserver 콜백 함수 const handleObserver = useCallback( - async (entries: IntersectionObserverEntry[]) => { + (entries: IntersectionObserverEntry[]) => { const target = entries[0]; - if (target.isIntersecting && !isFetchingNextPage && cursorId && !isSearching) { handleMoreInvitations(cursorId); } }, - [cursorId, isFetchingNextPage, isSearching], + [cursorId, isFetchingNextPage, isSearching, handleMoreInvitations], ); + // IntersectionObserver 설정 useEffect(() => { - const observer = new IntersectionObserver(handleObserver, { - threshold: 0.8, - }); - + const observer = new IntersectionObserver(handleObserver, { threshold: 0.8 }); const currentObserverRef = observerRef.current; - if (currentObserverRef) { observer.observe(currentObserverRef); } - return () => { if (currentObserverRef) { observer.unobserve(currentObserverRef); @@ -107,17 +107,20 @@ export default function InvitedDashboardList() { } }; - const handleChangeSearch = debounce(async (e: React.ChangeEvent) => { - const searchValue = e.target.value; - setIsSearching(!!searchValue); - - try { - const { data: searchData } = await getInvitationsList(10, 0, searchValue); - setInvitations(searchData.invitations); - } catch (err) { - console.error('데이터를 가져오는 중 오류가 발생했습니다:', err); - } - }, 300); + // 검색어 변경 처리 함수 + const handleChangeSearch = useCallback( + debounce(async (e: React.ChangeEvent) => { + const searchValue = e.target.value; + setIsSearching(!!searchValue); + try { + const { data: searchData } = await getInvitationsList(10, 0, searchValue); + setInvitations(searchData.invitations); + } catch (err) { + console.error('데이터를 가져오는 중 오류가 발생했습니다:', err); + } + }, 300), + [], + ); if (error) { return ( @@ -131,12 +134,14 @@ export default function InvitedDashboardList() { ); } + if (!invitations) return null; + return (

    초대받은 대시보드

    - {isLoading ? ( + {isLoading && !initialInvitedDashboard ? ( ) : ( <> @@ -152,7 +157,7 @@ export default function InvitedDashboardList() { ) : (
    - invitations + invitations

    초대된 대시보드가 없습니다.

    diff --git a/src/hooks/useFetchData.ts b/src/hooks/useFetchData.ts index 5dab6d74..993747a7 100644 --- a/src/hooks/useFetchData.ts +++ b/src/hooks/useFetchData.ts @@ -4,15 +4,16 @@ const useFetchData = ( queryKey: QueryKey, getService: () => Promise<{ data: T }>, refetchInterval: false | number = false, + handleSuccess?: () => void, ): UseQueryResult => { return useQuery({ queryKey: queryKey, queryFn: async () => { try { const response = await getService(); + handleSuccess && handleSuccess(); return response.data; } catch (error) { - // 에러 처리 throw new Error('데이터를 불러오는 중 에러 발생: ' + error); } }, diff --git a/src/hooks/useSignIn.ts b/src/hooks/useSignIn.ts index 66f9cfe1..6d5f787e 100644 --- a/src/hooks/useSignIn.ts +++ b/src/hooks/useSignIn.ts @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { AxiosError } from 'axios'; +import { setCookie } from 'cookies-next'; import { useDispatch } from 'react-redux'; import { postSignIn } from '@/services/postService'; @@ -22,6 +23,7 @@ export const useSignIn = () => { onSuccess: (data) => { dispatch(setUser(data)); dispatch(isLoading(false)); + setCookie('token', data.accessToken); }, onMutate: async () => { dispatch(isLoading(true)); diff --git a/src/hooks/useUserDropdown.ts b/src/hooks/useUserDropdown.ts index 43d88954..3b2389be 100644 --- a/src/hooks/useUserDropdown.ts +++ b/src/hooks/useUserDropdown.ts @@ -1,3 +1,4 @@ +import { deleteCookie } from 'cookies-next'; import { useRouter } from 'next/router'; import { MouseEventHandler, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -29,6 +30,7 @@ const useUserDropdown = () => { const handleLogoutClick: MouseEventHandler = (e) => { e.stopPropagation(); dispatch(clearUser()); + deleteCookie('token'); setIsOpen(false); router.push('/'); }; diff --git a/src/pages/mydashboard/index.tsx b/src/pages/mydashboard/index.tsx index a6806cd5..45d1a858 100644 --- a/src/pages/mydashboard/index.tsx +++ b/src/pages/mydashboard/index.tsx @@ -1,16 +1,58 @@ +import { getCookies } from 'cookies-next'; +import { GetServerSideProps } from 'next'; import Head from 'next/head'; import DashboardList from '@/containers/mydashboard/DashboardList'; import InvitedDashboardList from '@/containers/mydashboard/InvitedDashboardList'; +import instance from '@/services/axios'; +import { DashboardsResponse } from '@/types/Dashboard.interface'; +import { InvitationsResponse } from '@/types/Invitation.interface'; +import { createAuthHeaders } from '@/utils/createAuthHeaders'; -export default function MyDashboardPage() { +interface MyDashboardPageProps { + initialDashboard: DashboardsResponse; + initialInvitedDashboard: InvitationsResponse; +} + +export default function MyDashboardPage({ initialDashboard, initialInvitedDashboard }: MyDashboardPageProps) { return (
    Taskify | 내 대시보드 - - + +
    ); } + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const cookies = getCookies({ req, res }); + const token = cookies['token']; + + let initialDashboard: DashboardsResponse | null = null; + let initialInvitedDashboard: InvitationsResponse | null = null; + + if (token) { + const authHeaders = createAuthHeaders(token); + + try { + const [dashboardResponse, invitationsResponse] = await Promise.all([ + instance.get('/dashboards?navigationMethod=pagination&page=1&size=5', authHeaders), + instance.get('/invitations?size=10', authHeaders), + ]); + + initialDashboard = dashboardResponse.data; + initialInvitedDashboard = invitationsResponse.data; + } catch (error) { + console.error('Failed to fetch data:', error); + } + } + + return { + props: { + initialDashboard, + initialInvitedDashboard, + }, + }; +}; diff --git a/src/utils/createAuthHeaders.ts b/src/utils/createAuthHeaders.ts new file mode 100644 index 00000000..01df7ade --- /dev/null +++ b/src/utils/createAuthHeaders.ts @@ -0,0 +1,5 @@ +export const createAuthHeaders = (token: string) => ({ + headers: { + Authorization: `Bearer ${token}`, + }, +});