-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from 2 commits
7be8f0d
8b953d1
bd0ef9f
6e1df7c
4d719c5
f54fde4
1790ab1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,24 @@ | ||
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 [isInitial, setIsInitial] = useState<boolean>(!!initialDashboard); | ||
|
||
// 대시보드 데이터를 가져오는 커스텀 훅 사용 | ||
const { | ||
data: dashboardResponse, | ||
error, | ||
|
@@ -20,7 +27,19 @@ export default function DashboardList() { | |
getDashboardsList('pagination', currentChunk, 5), | ||
); | ||
|
||
const totalPage = dashboardResponse ? Math.max(1, Math.ceil(dashboardResponse.totalCount / 5)) : 1; | ||
// 총 페이지 수 계산 | ||
const totalPage = initialDashboard?.totalCount ? Math.max(1, Math.ceil(initialDashboard.totalCount / 5)) : 1; | ||
|
||
// 대시보드 데이터가 변경될 때 상태 업데이트 | ||
useEffect(() => { | ||
if (dashboardResponse) { | ||
if (!isInitial) { | ||
setDashboards(dashboardResponse); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 처음 진입 이후에 사용합니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 멘토님이 useEffect 쓸 필요 없이 useQuery에서 onSuccess일때 바꾸면 된다고 했던 것 같아요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아니면 제가 추후 SSR 적용하면서 같이 이런 식으로 바꿀게요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 어떻게 사용한다는 말씀일까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // 대시보드 데이터가 변경될 때 상태 업데이트
const handleSuccess = () => {
if (dashboardResponse) {
setDashboards(dashboardResponse);
}
};
// 대시보드 데이터를 가져오는 커스텀 훅 사용
const {
data: dashboardResponse,
error,
isLoading,
} = useFetchData<DashboardsResponse>(
['dashboards', currentChunk],
() => getDashboardsList('pagination', currentChunk, 5),
false,
handleSuccess,
); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오.. 리스폰스 받자마다 handleSuccess 했는데도 더 늦나요?!! 신기하네요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tanstack의 App.tsximport { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
...
// QueryCache로 글로벌 콜백 설정
const queryCache = new QueryCache({
onSuccess: (data) => {
console.log('Global onSuccess:', data);
// 여기에 글로벌 onSuccess 로직 추가할 수 있어요!
},
onError: (error) => {
console.error('Global onError:', error);
},
});
// QueryClient
const queryClient = new QueryClient({
queryCache,
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아까 어떤 글에서 query cache 봤는데, 어떻게 쓰는지 아직 몰라서 넘겼어요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵!! 전역적으로 적용해보면 좋을 것 같습니다! 이슈로 빼놓을까요?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋아요!! 새 이슈 만들고 얼른 머지해서 다른데서도 반영하는걸로 해요~~ |
||
} else { | ||
setIsInitial(false); | ||
} | ||
} | ||
}, [dashboardResponse, isInitial]); | ||
|
||
if (error) { | ||
return ( | ||
|
@@ -31,41 +50,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 && !isInitial | ||
? [...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} /> | ||
|
@@ -75,9 +97,7 @@ export default function DashboardList() { | |
</Link> | ||
</li> | ||
))} | ||
</> | ||
)} | ||
|
||
{/* 페이지네이션 */} | ||
<div className='md:col-span-2 lg:col-span-3 lg:row-start-3'> | ||
<Pagination | ||
currentChunk={currentChunk} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,71 +12,79 @@ 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 [isInitial, setIsInitial] = useState<boolean>(!initialInvitedDashboard); | ||
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); | ||
if (!isInitial) { | ||
setInvitations(data.invitations); | ||
setCursorId(data.cursorId ?? 0); | ||
} else { | ||
setIsInitial(false); | ||
} | ||
} | ||
}, [data]); | ||
}, [data, isInitial]); | ||
|
||
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); | ||
|
@@ -88,17 +96,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 ( | ||
|
@@ -112,10 +123,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 && !isInitial && !initialInvitedDashboard ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 초기 데이터가 있으면 스켈레톤이 작동하지 않습니다 |
||
<Skeleton /> | ||
) : ( | ||
<> | ||
|
@@ -140,4 +153,6 @@ export default function InvitedDashboardList() { | |
)} | ||
</section> | ||
); | ||
} | ||
}; | ||
|
||
export default InvitedDashboardList; |
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'; | ||
|
@@ -22,6 +23,7 @@ export const useSignIn = () => { | |
onSuccess: (data) => { | ||
dispatch(setUser(data)); | ||
dispatch(isLoading(false)); | ||
setCookie('token', data.accessToken); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인 할 때 쿠키로 액세스 토큰을 저장합니다 |
||
}, | ||
onMutate: async () => { | ||
dispatch(isLoading(true)); | ||
|
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'; | ||
|
@@ -29,6 +30,7 @@ const useUserDropdown = () => { | |
const handleLogoutClick: MouseEventHandler<HTMLButtonElement> = (e) => { | ||
e.stopPropagation(); | ||
dispatch(clearUser()); | ||
deleteCookie('token'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그아웃 할 때 쿠키를 제거합니다( 여기에 추가하는게 맞나요? ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵! 테스트하니 잘 지워지네요 |
||
setIsOpen(false); | ||
router.push('/'); | ||
}; | ||
|
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.
초기 데이터가 있는지 확인용