Skip to content

Commit

Permalink
♻️ refactor(#167): 내 대시보드 페이지 서버 사이드 렌더링 적용 (#199)
Browse files Browse the repository at this point in the history
* ♻️  refactor(#167): 내 대시보드 페이지 서버 사이드 렌더링 적용

* ♻️  refactor(#167): 내 대시보드 서버 사이드 렌더링 수정

* 🛠  fix(#167): 서버사이드 중복 코드 제거

* 🛠  fix(#167): 초대 목록 함수 수정

* 🛠  fix(#167): 이미지 불러오는 성능 개선

* 🛠  fix(#167): 컨플릭트 해결
  • Loading branch information
wjsdncl authored Jul 7, 2024
1 parent 66d3ee2 commit 7f4c6b8
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 59 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 31 additions & 25 deletions src/containers/mydashboard/DashboardList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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';
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<number>(1);
const { openNewDashboardModal } = useModal();
const [dashboardData, setDashboards] = useState<DashboardsResponse>(initialDashboard);

const {
data: dashboardResponse,
Expand All @@ -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 (
Expand All @@ -31,43 +44,45 @@ 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 (
<section className='flex-col justify-between'>
<ul className='grid max-w-[350px] grid-rows-1 gap-3 font-semibold text-black-33 md:min-h-[216px] md:max-w-full md:grid-cols-2 md:grid-rows-3 lg:min-h-[140px] lg:max-w-screen-lg lg:grid-cols-3 dark:text-dark-10'>
{/* 새로운 대시보드 생성 버튼 */}
<li className='h-12 w-full rounded-lg border border-gray-d9 md:h-16 dark:border-dark-200'>
<button
className='btn-violet-light dark:btn-violet-dark size-full gap-4'
type='button'
onClick={() => openNewDashboardModal()}
>
새로운 대시보드
<Image src={'/icons/plus-filled.svg'} alt='plus' width={22} height={22} className='dark:hidden' />
<Image src={'/icons/plus.svg'} alt='plus' width={22} height={22} className='hidden dark:block' />
<Image src={'/icons/plus-filled.svg'} alt='plus' width={22} height={22} />
</button>
</li>
{isLoading ? (
<>
{[...Array(5)].map((_, i) => (
{/* 대시보드 목록 표시 */}
{isLoading && currentChunk !== 1
? [...Array(5)].map((_, i) => (
<li
key={i}
className='h-12 w-full animate-pulse rounded-lg border border-gray-d9 bg-gray-fa md:h-16 dark:border-dark-200 dark:bg-dark-300'
/>
))}
</>
) : (
<>
{dashboardResponse?.dashboards.map((dashboard) => (
))
: dashboardData.dashboards.map((dashboard) => (
<li
className='h-12 w-full rounded-lg border border-gray-d9 md:h-16 dark:border-dark-200'
key={dashboard.id}
Expand All @@ -79,26 +94,17 @@ export default function DashboardList() {
<div className='flex size-full items-center'>
<div className='rounded-full p-1' style={{ backgroundColor: dashboard.color }} />
<div className='mx-4 h-[28px] grow overflow-hidden text-ellipsis text-lg font-medium'>
<p className={`size-full`}>{dashboard.title}</p>
<p className='size-full'>{dashboard.title}</p>
</div>
{dashboard.createdByMe && (
<Image src={'/icons/crown.svg'} className='mr-3' alt='my' width={20} height={16} />
)}
</div>
<Image src={'/icons/arrow-black.svg'} alt='arrow' width={14} height={14} className='dark:hidden' />
<Image
src={'/icons/arrow-white.svg'}
alt='arrow'
width={7}
height={7}
className='hidden dark:block'
/>
<Image src={'/icons/arrow-black.svg'} alt='arrow' width={14} height={14} />
</Link>
</li>
))}
</>
)}

{/* 페이지네이션 */}
<div className='md:col-span-2 lg:col-span-3 lg:row-start-3'>
<Pagination
currentChunk={currentChunk}
Expand Down
65 changes: 35 additions & 30 deletions src/containers/mydashboard/InvitedDashboardList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import { getInvitationsList } from '@/services/getService';
import { putAcceptInvitation } from '@/services/putService';
import { Invitation, InvitationsResponse } from '@/types/Invitation.interface';

export default function InvitedDashboardList() {
const [invitations, setInvitations] = useState<Invitation[]>([]);
interface InvitedDashboardListProps {
initialInvitedDashboard: InvitationsResponse;
}

export default function InvitedDashboardList({ initialInvitedDashboard }: InvitedDashboardListProps) {
const [invitations, setInvitations] = useState<Invitation[]>(initialInvitedDashboard?.invitations || []);
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -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);
Expand Down Expand Up @@ -107,17 +107,20 @@ export default function InvitedDashboardList() {
}
};

const handleChangeSearch = debounce(async (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
Expand All @@ -131,12 +134,14 @@ export default function InvitedDashboardList() {
);
}

if (!invitations) return null;

return (
<section className='h-[400px] max-w-[350px] rounded-lg border-0 bg-white transition-colors md:max-h-[740px] md:min-h-[530px] md:max-w-full lg:max-w-screen-lg dark:bg-dark'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33 transition-colors dark:text-dark-10'>
초대받은 대시보드
</p>
{isLoading ? (
{isLoading && !initialInvitedDashboard ? (
<Skeleton />
) : (
<>
Expand All @@ -152,7 +157,7 @@ export default function InvitedDashboardList() {
) : (
<div className='flex flex-col items-center justify-center py-[100px]'>
<div className='relative size-[60px] md:size-[150px]'>
<Image src={'/icons/invitations.svg'} alt='invitations' fill />
<Image src={'/icons/invitations.svg'} alt='invitations' fill priority />
</div>
<p className='px-7 py-5 text-sm text-gray-78 md:text-base'>초대된 대시보드가 없습니다.</p>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useFetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ const useFetchData = <T>(
queryKey: QueryKey,
getService: () => Promise<{ data: T }>,
refetchInterval: false | number = false,
handleSuccess?: () => void,
): UseQueryResult<T, Error> => {
return useQuery<T, Error>({
queryKey: queryKey,
queryFn: async () => {
try {
const response = await getService();
handleSuccess && handleSuccess();
return response.data;
} catch (error) {
// 에러 처리
throw new Error('데이터를 불러오는 중 에러 발생: ' + error);
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useSignIn.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,6 +23,7 @@ export const useSignIn = () => {
onSuccess: (data) => {
dispatch(setUser(data));
dispatch(isLoading(false));
setCookie('token', data.accessToken);
},
onMutate: async () => {
dispatch(isLoading(true));
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useUserDropdown.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,6 +30,7 @@ const useUserDropdown = () => {
const handleLogoutClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
dispatch(clearUser());
deleteCookie('token');
setIsOpen(false);
router.push('/');
};
Expand Down
Loading

0 comments on commit 7f4c6b8

Please sign in to comment.