From 9694787b3a6a9ca698c2ee9519a2820a3235bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:37:24 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Feat:=20=ED=97=A4=EB=8D=94=EC=97=90=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Header/UserProfileForm.tsx | 46 +++++++++++++++++++ .../components/Header/UserProfileModal.tsx | 31 +++++++++++++ frontend/src/components/Header/index.tsx | 5 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Header/UserProfileForm.tsx create mode 100644 frontend/src/components/Header/UserProfileModal.tsx diff --git a/frontend/src/components/Header/UserProfileForm.tsx b/frontend/src/components/Header/UserProfileForm.tsx new file mode 100644 index 0000000..584290b --- /dev/null +++ b/frontend/src/components/Header/UserProfileForm.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { Button } from '../ui/button'; +import useAuthStore from '@/stores/useAuthStore'; +import useLogoutQuery from '@/queries/auth/useLogoutQuery'; +export default function UserProfileForm({ onClose }: { onClose: () => void }) { + const profile = useAuthStore((state) => state.profile); + const { nickname, profileImage } = profile || { nickname: '', profileImage: '' }; + + const logoutMutation = useLogoutQuery(); + const [isLoggingOut, setIsLoggingOut] = useState(false); + + const handleLogout = async () => { + setIsLoggingOut(true); + logoutMutation.mutate(undefined, { + onSuccess: () => { + onClose(); + }, + }); + }; + return ( +
+
+ {profileImage ? ( + {`${nickname}'s + ) : ( +
+ )} + +
{nickname || 'Guest'}
+
+ + +
+ ); +} diff --git a/frontend/src/components/Header/UserProfileModal.tsx b/frontend/src/components/Header/UserProfileModal.tsx new file mode 100644 index 0000000..9e92679 --- /dev/null +++ b/frontend/src/components/Header/UserProfileModal.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; +import { User } from 'lucide-react'; +import UserProfileForm from './UserProfileForm'; + +export default function UserProfileModal() { + const [isOpen, setIsOpen] = React.useState(false); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + + return ( + + + + + + + + + + ); +} diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 0375839..d1a2fab 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -import { Bell, User } from 'lucide-react'; +import { Bell } from 'lucide-react'; import { useLocation, Link, useParams } from 'react-router-dom'; +import UserProfileModal from './UserProfileModal'; export interface HeaderProps extends React.HTMLAttributes {} @@ -60,7 +61,7 @@ export default function Header({ className, ...props }: HeaderProps) { {!isHomePage && (
- +
)} From 9598ee0f19575baa1ad0dc31380b3a7422c7bdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:38:16 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Refactor:=20Adminlayout=20overflow=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AdminLayout/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AdminLayout/index.tsx b/frontend/src/components/AdminLayout/index.tsx index a413caa..80e6f89 100644 --- a/frontend/src/components/AdminLayout/index.tsx +++ b/frontend/src/components/AdminLayout/index.tsx @@ -13,7 +13,7 @@ export default function AdminLayout() { -
+
From 80091081197c78014db20e97ddfdcaf55c5a6473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:39:52 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Test:=20msw=20=EB=A6=AC=EB=B7=B0=20=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/reviewHandlers.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/mocks/reviewHandlers.ts b/frontend/src/mocks/reviewHandlers.ts index 1c65479..3e5e222 100644 --- a/frontend/src/mocks/reviewHandlers.ts +++ b/frontend/src/mocks/reviewHandlers.ts @@ -125,19 +125,22 @@ export const reviewHandlers = [ const projectId = Array.isArray(params.projectId) ? parseInt(params.projectId[0], 10) : parseInt(params.projectId as string, 10); - console.log(projectId); const reviewStatus = Array.isArray(params.reviewStatus) ? params.reviewStatus[0] : params.reviewStatus; - const lastReviewId = Array.isArray(params.lastReviewId) ? params.lastReviewId[0] : params.lastReviewId; + const lastReviewId = Array.isArray(params.lastReviewId) + ? parseInt(params.lastReviewId[0], 10) + : parseInt(params.lastReviewId as string, 10) || 0; const limitPage = Array.isArray(params.limitPage) ? parseInt(params.limitPage[0], 10) : parseInt(params.limitPage as string, 10) || 10; - const reviews: ReviewResponse[] = Array.from({ length: limitPage }, (_, index) => ({ + // 총 100개의 리뷰를 생성 + const totalReviews = 100; + const reviews: ReviewResponse[] = Array.from({ length: totalReviews }, (_, index) => ({ projectId, - reviewId: lastReviewId ? parseInt(lastReviewId, 10) + index : index + 1, + reviewId: index + 1, title: `Review ${index + 1}`, content: `Review content ${index + 1}`, status: (reviewStatus || 'REQUESTED') as 'REQUESTED' | 'APPROVED' | 'REJECTED', @@ -146,6 +149,10 @@ export const reviewHandlers = [ author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' }, })); - return HttpResponse.json(reviews); + // 마지막 리뷰 ID 기준으로 데이터를 잘라서 반환 + const startIndex = lastReviewId > 0 ? lastReviewId : 0; + const slicedReviews = reviews.slice(startIndex, startIndex + limitPage); + + return HttpResponse.json(slicedReviews); }), ]; From cf97dc4d20c5ed6b09d312a077b882fb3d1d4244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:41:17 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/queries/auth/useLogoutQuery.ts | 16 ++++++++++++++ .../queries/reviews/useReviewByStatusQuery.ts | 15 ++++++++++--- .../workspaces/useWorkspaceReviewsQuery.tsx | 21 +++++++++++++------ 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 frontend/src/queries/auth/useLogoutQuery.ts diff --git a/frontend/src/queries/auth/useLogoutQuery.ts b/frontend/src/queries/auth/useLogoutQuery.ts new file mode 100644 index 0000000..2379128 --- /dev/null +++ b/frontend/src/queries/auth/useLogoutQuery.ts @@ -0,0 +1,16 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import useAuthStore from '@/stores/useAuthStore'; +import { logout } from '@/api/authApi'; + +export default function useLogoutQuery() { + const queryClient = useQueryClient(); + const { clearAuth } = useAuthStore(); + + return useMutation({ + mutationFn: logout, + onSuccess: () => { + clearAuth(); + queryClient.invalidateQueries({ queryKey: ['profile'] }); + }, + }); +} diff --git a/frontend/src/queries/reviews/useReviewByStatusQuery.ts b/frontend/src/queries/reviews/useReviewByStatusQuery.ts index d677d8e..97793d7 100644 --- a/frontend/src/queries/reviews/useReviewByStatusQuery.ts +++ b/frontend/src/queries/reviews/useReviewByStatusQuery.ts @@ -1,13 +1,22 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { getReviewByStatus } from '@/api/reviewApi'; -import { useSuspenseQuery } from '@tanstack/react-query'; +import { ReviewResponse } from '@/types'; export default function useReviewByStatusQuery( projectId: number, memberId: number, reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED' | undefined ) { - return useSuspenseQuery({ + return useSuspenseInfiniteQuery({ queryKey: ['reviewByStatus', projectId, reviewStatus], - queryFn: () => getReviewByStatus(projectId, memberId, reviewStatus), + queryFn: ({ pageParam = undefined }) => { + return getReviewByStatus(projectId, memberId, reviewStatus, pageParam as number | undefined); + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + const lastReview = lastPage[lastPage.length - 1]; + return lastReview.reviewId; + }, + initialPageParam: undefined, }); } diff --git a/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx b/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx index 2e03385..7832914 100644 --- a/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx +++ b/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx @@ -1,15 +1,24 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { getWorkspaceReviews } from '@/api/workspaceApi'; -import { useSuspenseQuery } from '@tanstack/react-query'; +import { ReviewResponse } from '@/types'; export default function useWorkspaceReviewsQuery( workspaceId: number, memberId: number, reviewStatus?: 'REQUESTED' | 'APPROVED' | 'REJECTED', - lastReviewId?: number, - limitPage?: number + limitPage: number = 10 ) { - return useSuspenseQuery({ - queryKey: ['workspaceReviews', workspaceId, reviewStatus, lastReviewId], - queryFn: () => getWorkspaceReviews(workspaceId, memberId, reviewStatus, lastReviewId, limitPage), + return useSuspenseInfiniteQuery({ + queryKey: ['workspaceReviews', workspaceId, reviewStatus], + queryFn: ({ pageParam = undefined }) => + getWorkspaceReviews(workspaceId, memberId, reviewStatus, pageParam as number | undefined, limitPage), + + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + const lastReview = lastPage[lastPage.length - 1]; + return lastReview.reviewId; + }, + + initialPageParam: undefined, }); } From 3d16081a6dfbfc6f98018e8c1fd70ad5eb72df82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:41:43 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Feat:=20=EC=BF=BC=EB=A6=AC=20=EC=9D=B8?= =?UTF-8?q?=ED=94=BC=EB=8B=88=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ProjectReviewList.tsx | 39 +++++++++++++++++++-- frontend/src/pages/WorkspaceReviewList.tsx | 40 ++++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/ProjectReviewList.tsx b/frontend/src/pages/ProjectReviewList.tsx index 405743d..e4fe4af 100644 --- a/frontend/src/pages/ProjectReviewList.tsx +++ b/frontend/src/pages/ProjectReviewList.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useParams, Link } from 'react-router-dom'; import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery'; import useAuthStore from '@/stores/useAuthStore'; @@ -14,12 +14,40 @@ export default function ProjectReviewList() { const [, setSearchQuery] = useState(''); const [sortValue, setSortValue] = useState('latest'); - const { data: projectReviews = [] } = useReviewByStatusQuery( + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useReviewByStatusQuery( Number(projectId), memberId, activeTab !== 'all' ? activeTab : undefined ); + const projectReviews = data?.pages.flat() || []; + + const loadMoreRef = useRef(null); + + useEffect(() => { + if (!hasNextPage || isFetchingNextPage) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + fetchNextPage(); + } + }, + { threshold: 1.0 } + ); + + const currentLoadMoreRef = loadMoreRef.current; + if (currentLoadMoreRef) { + observer.observe(currentLoadMoreRef); + } + + return () => { + if (currentLoadMoreRef) { + observer.unobserve(currentLoadMoreRef); + } + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + return (
@@ -41,6 +69,13 @@ export default function ProjectReviewList() { setSortValue={setSortValue} workspaceId={Number(workspaceId)} /> + + {isFetchingNextPage &&
로딩 중...
} + +
); } diff --git a/frontend/src/pages/WorkspaceReviewList.tsx b/frontend/src/pages/WorkspaceReviewList.tsx index 92f0a7c..3f4edf1 100644 --- a/frontend/src/pages/WorkspaceReviewList.tsx +++ b/frontend/src/pages/WorkspaceReviewList.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useParams, Link } from 'react-router-dom'; import useWorkspaceReviewsQuery from '@/queries/workspaces/useWorkspaceReviewsQuery'; import useAuthStore from '@/stores/useAuthStore'; @@ -14,12 +14,40 @@ export default function WorkspaceReviewList() { const [, setSearchQuery] = useState(''); const [sortValue, setSortValue] = useState('latest'); - const { data: workspaceReviews = [] } = useWorkspaceReviewsQuery( + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useWorkspaceReviewsQuery( Number(workspaceId), memberId, activeTab !== 'all' ? activeTab : undefined ); + const workspaceReviews = data?.pages.flat() || []; + + const loadMoreRef = useRef(null); + + useEffect(() => { + if (!hasNextPage || isFetchingNextPage) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + fetchNextPage(); + } + }, + { threshold: 1.0 } + ); + + const currentLoadMoreRef = loadMoreRef.current; + if (currentLoadMoreRef) { + observer.observe(currentLoadMoreRef); + } + + return () => { + if (currentLoadMoreRef) { + observer.unobserve(currentLoadMoreRef); + } + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + return (
@@ -31,6 +59,7 @@ export default function WorkspaceReviewList() {
+ + + {isFetchingNextPage &&
로딩 중...
} + +
); } From 04cb52e79c7086ebb4f5cbfed904e81b0eefd7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Thu, 26 Sep 2024 03:59:05 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ProjectReviewList.tsx | 54 +++++++++++----------- frontend/src/pages/WorkspaceReviewList.tsx | 54 +++++++++++----------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/frontend/src/pages/ProjectReviewList.tsx b/frontend/src/pages/ProjectReviewList.tsx index e4fe4af..4fa219c 100644 --- a/frontend/src/pages/ProjectReviewList.tsx +++ b/frontend/src/pages/ProjectReviewList.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { Suspense, useState, useEffect, useRef } from 'react'; import { useParams, Link } from 'react-router-dom'; import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery'; import useAuthStore from '@/stores/useAuthStore'; @@ -49,33 +49,35 @@ export default function ProjectReviewList() { }, [hasNextPage, isFetchingNextPage, fetchNextPage]); return ( -
-
-

프로젝트 리뷰

- - - -
+
}> +
+
+

프로젝트 리뷰

+ + + +
- + - {isFetchingNextPage &&
로딩 중...
} + {isFetchingNextPage} -
-
+
+
+ ); } diff --git a/frontend/src/pages/WorkspaceReviewList.tsx b/frontend/src/pages/WorkspaceReviewList.tsx index 3f4edf1..9c01c1c 100644 --- a/frontend/src/pages/WorkspaceReviewList.tsx +++ b/frontend/src/pages/WorkspaceReviewList.tsx @@ -4,7 +4,7 @@ import useWorkspaceReviewsQuery from '@/queries/workspaces/useWorkspaceReviewsQu import useAuthStore from '@/stores/useAuthStore'; import ReviewList from '@/components/ReviewList'; import { Button } from '@/components/ui/button'; - +import { Suspense } from 'react'; export default function WorkspaceReviewList() { const { workspaceId } = useParams<{ workspaceId: string }>(); const profile = useAuthStore((state) => state.profile); @@ -49,33 +49,35 @@ export default function WorkspaceReviewList() { }, [hasNextPage, isFetchingNextPage, fetchNextPage]); return ( -
-
-

워크스페이스 리뷰

- - - -
+
}> +
+
+

워크스페이스 리뷰

+ + + +
- + - {isFetchingNextPage &&
로딩 중...
} + {isFetchingNextPage} -
-
+
+
+ ); }