diff --git a/frontend/src/apis/endpoints.ts b/frontend/src/apis/endpoints.ts index ab5e0ebb4..95bfb3174 100644 --- a/frontend/src/apis/endpoints.ts +++ b/frontend/src/apis/endpoints.ts @@ -5,6 +5,8 @@ export const DETAILED_REVIEW_API_PARAMS = { }, }; +export const DETAILED_REVIEW_API_URL = `${process.env.API_BASE_URL}/${DETAILED_REVIEW_API_PARAMS.resource}`; + export const REVIEW_WRITING_API_PARAMS = { queryString: { reviewRequestCode: 'reviewRequestCode', @@ -14,7 +16,7 @@ export const REVIEW_WRITING_API_PARAMS = { const endPoint = { postingReview: `${process.env.API_BASE_URL}/reviews`, gettingDetailedReview: (reviewId: number, memberId: number) => - `${process.env.API_BASE_URL}/${DETAILED_REVIEW_API_PARAMS.resource}/${reviewId}?${DETAILED_REVIEW_API_PARAMS.queryString.memberId}=${memberId}`, + `${DETAILED_REVIEW_API_URL}/${reviewId}?${DETAILED_REVIEW_API_PARAMS.queryString.memberId}=${memberId}`, gettingDataToWriteReview: (reviewRequestCode: string) => `${process.env.API_BASE_URL}/reviews/write?${REVIEW_WRITING_API_PARAMS.queryString.reviewRequestCode}=${reviewRequestCode}`, gettingReviewList: `${process.env.API_BASE_URL}/reviews`, diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 29d781aff..82dc1f0eb 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,3 +1,5 @@ export { default as useSidebar } from './useSidebar'; export { default as useModalClose } from './useModalClose'; export { default as useGroupAccessCode } from './useGroupAccessCode'; +export { default as useGetDetailedReview } from './review/useGetDetailedReview'; +export { default as useSearchParamAndQuery } from './useSearchParamAndQuery'; diff --git a/frontend/src/hooks/review/useGetDetailedReview/index.ts b/frontend/src/hooks/review/useGetDetailedReview/index.ts new file mode 100644 index 000000000..9d051f965 --- /dev/null +++ b/frontend/src/hooks/review/useGetDetailedReview/index.ts @@ -0,0 +1,33 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getDetailedReviewApi } from '@/apis/review'; +import { REVIEW_QUERY_KEYS } from '@/constants'; +import { DetailReviewData } from '@/types'; + +interface UseGetDetailedReviewProps { + reviewId: number; + memberId: number; + groupAccessCode: string; +} + +interface FetchDetailedReviewParams { + reviewId: number; + memberId: number; + groupAccessCode: string; +} + +const useGetDetailedReview = ({ reviewId, memberId, groupAccessCode }: UseGetDetailedReviewProps) => { + const fetchDetailedReview = async ({ reviewId, memberId, groupAccessCode }: FetchDetailedReviewParams) => { + const result = await getDetailedReviewApi({ reviewId, memberId, groupAccessCode }); + return result; + }; + + const result = useSuspenseQuery({ + queryKey: [REVIEW_QUERY_KEYS.detailedReview, reviewId, memberId], + queryFn: () => fetchDetailedReview({ reviewId, memberId, groupAccessCode }), + }); + + return result; +}; + +export default useGetDetailedReview; diff --git a/frontend/src/hooks/review/useGetDetailedReview/test.ts b/frontend/src/hooks/review/useGetDetailedReview/test.ts new file mode 100644 index 000000000..acecd053a --- /dev/null +++ b/frontend/src/hooks/review/useGetDetailedReview/test.ts @@ -0,0 +1,26 @@ +import { renderHook, waitFor } from '@testing-library/react'; + +import { DETAILED_PAGE_MOCK_API_SETTING_VALUES } from '@/mocks/mockData/detailedReviewMockData'; +import QueryClientWrapper from '@/queryTestSetup/QueryClientWrapper'; + +import useGetDetailedReview from '.'; +// 아래의 테스트는 로그인이 유효하다는 가정하에서 진행 + +describe('리뷰 상세페이지 데이터 요청 테스트', () => { + const GROUND_ACCESS_CODE = '1234'; + + it('유효힌 id,memberId 사용해야 라뷰 상세 페이지 데이터를 불러온다.', async () => { + const { reviewId, memberId } = DETAILED_PAGE_MOCK_API_SETTING_VALUES; + + const { result } = renderHook( + () => useGetDetailedReview({ reviewId, memberId, groupAccessCode: GROUND_ACCESS_CODE }), + { wrapper: QueryClientWrapper }, + ); + + await waitFor(() => { + expect(result.current.status).toBe('success'); + }); + + expect(result.current.data).toBeDefined(); + }); +}); diff --git a/frontend/src/hooks/useSearchParamAndQuery.ts b/frontend/src/hooks/useSearchParamAndQuery.ts new file mode 100644 index 000000000..349d9a6e5 --- /dev/null +++ b/frontend/src/hooks/useSearchParamAndQuery.ts @@ -0,0 +1,22 @@ +import { useLocation, useParams } from 'react-router'; + +interface UseSearchParamAndQueryProps { + paramKey: string; + queryStringKey?: string; +} +/** + * url에서 원하는 param, queryString의 값을 가져온다. + * @param paramKey: 가져오고 싶은 param의 key + * @param queryStringKey: 가져오고 싶은 queryString의 key (옵셔널) + */ +const useSearchParamAndQuery = ({ paramKey, queryStringKey }: UseSearchParamAndQueryProps) => { + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + + return { + param: useParams()[`${paramKey}`], + queryString: queryStringKey ? queryParams.get(queryStringKey) : null, + }; +}; + +export default useSearchParamAndQuery; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index a4a50d4b5..920428ac0 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -48,7 +48,7 @@ const router = createBrowserRouter([ element: , }, { - path: 'user/detailed-review/:id', + path: 'user/detailed-review/:reviewId', element: , }, { diff --git a/frontend/src/mocks/handlers/review.ts b/frontend/src/mocks/handlers/review.ts index ae8c92dde..7541ef524 100644 --- a/frontend/src/mocks/handlers/review.ts +++ b/frontend/src/mocks/handlers/review.ts @@ -1,12 +1,8 @@ import { http, HttpResponse } from 'msw'; -import endPoint from '@/apis/endpoints'; +import endPoint, { DETAILED_REVIEW_API_PARAMS, DETAILED_REVIEW_API_URL } from '@/apis/endpoints'; -import { - DETAILED_REVIEW_MOCK_DATA, - DETAILED_PAGE_MOCK_API_SETTING_VALUES, - DETAILED_PAGE_ERROR_API_VALUES, -} from '../mockData/detailedReviewMockData'; +import { DETAILED_REVIEW_MOCK_DATA, DETAILED_PAGE_MOCK_API_SETTING_VALUES } from '../mockData/detailedReviewMockData'; import { REVIEW_PREVIEW_LIST } from '../mockData/reviewPreviewList'; import { REVIEW_WRITING_DATA } from '../mockData/reviewWritingData'; @@ -18,23 +14,20 @@ export const PAGE = { }; const getDetailedReview = () => - http.get( - endPoint.gettingDetailedReview( - DETAILED_PAGE_MOCK_API_SETTING_VALUES.reviewId, - DETAILED_PAGE_MOCK_API_SETTING_VALUES.memberId, - ), - async () => { + http.get(new RegExp(`^${DETAILED_REVIEW_API_URL}/\\d+$`), async ({ request }) => { + //요청 url에서 reviewId, memberId 추출 + const url = new URL(request.url); + const urlReviewId = url.pathname.replace(`/${DETAILED_REVIEW_API_PARAMS.resource}/`, ''); + const urlMemberId = url.searchParams.get(DETAILED_REVIEW_API_PARAMS.queryString.memberId); + + const { reviewId, memberId } = DETAILED_PAGE_MOCK_API_SETTING_VALUES; + // 유효한 reviewId, memberId일 경우에만 데이터 반환 + if (Number(urlReviewId) == reviewId && Number(urlMemberId) === memberId) { return HttpResponse.json(DETAILED_REVIEW_MOCK_DATA); - }, - ); + } -const getWrongDetailReview = () => - http.get( - endPoint.gettingDetailedReview(DETAILED_PAGE_ERROR_API_VALUES.reviewId, DETAILED_PAGE_ERROR_API_VALUES.memberId), - async () => { - return HttpResponse.json({ error: '잘못된 상세리뷰 요청' }, { status: 404 }); - }, - ); + return HttpResponse.json({ error: '잘못된 상세리뷰 요청' }, { status: 404 }); + }); const getDataToWriteReview = () => http.get(endPoint.gettingDataToWriteReview(10), async ({ request }) => { @@ -68,6 +61,6 @@ const getReviewPreviewList = () => { }); }; -const reviewHandler = [getDetailedReview(), getWrongDetailReview(), getReviewPreviewList(), getDataToWriteReview()]; +const reviewHandler = [getDetailedReview(), getReviewPreviewList(), getDataToWriteReview()]; export default reviewHandler; diff --git a/frontend/src/mocks/mockData/detailedReviewMockData.ts b/frontend/src/mocks/mockData/detailedReviewMockData.ts index 1eb26266d..e0d2a8f48 100644 --- a/frontend/src/mocks/mockData/detailedReviewMockData.ts +++ b/frontend/src/mocks/mockData/detailedReviewMockData.ts @@ -5,11 +5,6 @@ export const DETAILED_PAGE_MOCK_API_SETTING_VALUES = { memberId: 2, }; -export const DETAILED_PAGE_ERROR_API_VALUES = { - reviewId: 0, - memberId: 0, -}; - const ANSWER = '림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. 림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. '; diff --git a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx index 04bdf4662..abaabc85d 100644 --- a/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/components/DetailedReviewPageContents/index.tsx @@ -1,12 +1,6 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { useLocation, useParams } from 'react-router'; - import { DETAILED_REVIEW_API_PARAMS } from '@/apis/endpoints'; -import { getDetailedReviewApi } from '@/apis/review'; -import { LoginRedirectModal } from '@/components'; -import { REVIEW_QUERY_KEYS } from '@/constants'; +import { useGetDetailedReview, useSearchParamAndQuery } from '@/hooks'; import { ReviewDescription, ReviewSection, KeywordSection } from '@/pages/DetailedReviewPage/components'; -import { DetailReviewData } from '@/types'; import * as S from './styles'; @@ -14,23 +8,17 @@ interface DetailedReviewPageContentsProps { groupAccessCode: string; } const DetailedReviewPageContents = ({ groupAccessCode }: DetailedReviewPageContentsProps) => { - const { id } = useParams<{ id: string }>(); - const location = useLocation(); - const queryParams = new URLSearchParams(location.search); - const memberId = queryParams.get(DETAILED_REVIEW_API_PARAMS.queryString.memberId); - - const fetchDetailedReview = async (reviewId: number, memberId: number, groupAccessCode: string) => { - const result = await getDetailedReviewApi({ reviewId, memberId, groupAccessCode }); - return result; - }; + const { param: reviewId, queryString: memberId } = useSearchParamAndQuery({ + paramKey: 'reviewId', + queryStringKey: DETAILED_REVIEW_API_PARAMS.queryString.memberId, + }); - const { data: detailedReview } = useSuspenseQuery({ - queryKey: [REVIEW_QUERY_KEYS.detailedReview, id, memberId], - queryFn: () => fetchDetailedReview(Number(id), Number(memberId), groupAccessCode), + const { data: detailedReview } = useGetDetailedReview({ + reviewId: Number(reviewId), + memberId: Number(memberId), + groupAccessCode, }); - if (!detailedReview) throw new Error(' 상세보기 리뷰 데이터를 가져올 수 없어요.'); - if (!groupAccessCode) return ; // TODO: 리뷰 공개/비공개 토글 버튼 기능 return ( diff --git a/frontend/src/pages/DetailedReviewPage/index.tsx b/frontend/src/pages/DetailedReviewPage/index.tsx index dd4827081..39e51a0ff 100644 --- a/frontend/src/pages/DetailedReviewPage/index.tsx +++ b/frontend/src/pages/DetailedReviewPage/index.tsx @@ -6,16 +6,12 @@ import { DetailedReviewPageContents } from './components'; const DetailedReviewPage = () => { const { groupAccessCode } = useGroupAccessCode(); + if (!groupAccessCode) return ; + return ( - <> - {groupAccessCode ? ( - - - - ) : ( - - )} - + + + ); }; diff --git a/frontend/src/queryTestSetup/QueryClientWrapper.tsx b/frontend/src/queryTestSetup/QueryClientWrapper.tsx new file mode 100644 index 000000000..eda49295d --- /dev/null +++ b/frontend/src/queryTestSetup/QueryClientWrapper.tsx @@ -0,0 +1,11 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { EssentialPropsWithChildren } from '@/types'; + +const queryClient = new QueryClient(); + +const QueryClientWrapper = ({ children }: EssentialPropsWithChildren) => ( + {children} +); + +export default QueryClientWrapper;