Skip to content
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

♻️ refactor(#167): 내 대시보드 페이지 서버 사이드 렌더링 적용 #199

Merged
merged 7 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
"next": "14.2.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.

47 changes: 31 additions & 16 deletions src/containers/mydashboard/DashboardList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
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,
error,
Expand All @@ -20,7 +26,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]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isInit state 뺐습니다 데이터를 새로 불러와서 집어넣어도 생긴게 동일해서 깜빡이거나 하지 않더라구요
초기값이 없으면 어짜피 얘도 없으니 그에 대한 처리도 안해줬습니다


if (error) {
return (
Expand All @@ -31,41 +45,44 @@ 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'>
{/* 새로운 대시보드 생성 버튼 */}
<li className='h-12 w-full rounded-lg border border-gray-d9 bg-white md:h-16'>
<button className='btn-violet-light size-full gap-4' type='button' onClick={() => openNewDashboardModal()}>
새로운 대시보드
<Image src={'/icons/plus-filled.svg'} alt='plus' width={22} height={22} />
</button>
</li>
{isLoading ? (
<>
{[...Array(5)].map((_, i) => (
{/* 대시보드 목록 표시 */}
{isLoading && currentChunk !== 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 페이지가 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' />
))}
</>
) : (
<>
{dashboardResponse?.dashboards.map((dashboard) => (
))
: dashboardData.dashboards.map((dashboard) => (
<li className='h-12 w-full rounded-lg border border-gray-d9 bg-white md:h-16' key={dashboard.id}>
<Link href={`/dashboard/${dashboard.id}`} className={'btn-violet-light size-full rounded-md px-5'}>
<Link href={`/dashboard/${dashboard.id}`} className='btn-violet-light size-full rounded-md px-5'>
<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} />
Expand All @@ -75,9 +92,7 @@ export default function DashboardList() {
</Link>
</li>
))}
</>
)}

{/* 페이지네이션 */}
<div className='md:col-span-2 lg:col-span-3 lg:row-start-3'>
<Pagination
currentChunk={currentChunk}
Expand Down
70 changes: 39 additions & 31 deletions src/containers/mydashboard/InvitedDashboardList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,71 +12,72 @@ 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;
}

const 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);
const [cursorId, setCursorId] = useState<number>(0);

const queryClient = useQueryClient();

const { data, error, isLoading } = useFetchData<InvitationsResponse>(['invitations'], () => getInvitationsList());
const { data, error, isLoading } = useFetchData<InvitationsResponse>(['invitations'], getInvitationsList);

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);
}
};
}, [observerRef, handleObserver]);

// 초대 수락/거절 처리 함수
const handleAcceptInvitation = async (invitationId: number, inviteAccepted: boolean) => {
try {
await putAcceptInvitation(invitationId, inviteAccepted);
Expand All @@ -88,17 +89,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 @@ -112,10 +116,12 @@ export default function InvitedDashboardList() {
);
}

if (!invitations) return null;

return (
<section className='h-[400px] max-w-[350px] rounded-lg border-0 bg-white md:max-h-[740px] md:min-h-[530px] md:max-w-full lg:max-w-screen-lg'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33'>초대받은 대시보드</p>
{isLoading ? (
{isLoading && !initialInvitedDashboard ? (
<Skeleton />
) : (
<>
Expand All @@ -140,4 +146,6 @@ export default function InvitedDashboardList() {
)}
</section>
);
}
};

export default InvitedDashboardList;
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 할 때 쿠키로 액세스 토큰을 저장합니다

},
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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그아웃 할 때 쿠키를 제거합니다( 여기에 추가하는게 맞나요? )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 테스트하니 잘 지워지네요

setIsOpen(false);
router.push('/');
};
Expand Down
Loading