From 36ebd7a4023cbcfa3ad2bc2e5d99ba4cde0144b6 Mon Sep 17 00:00:00 2001 From: badahertz52 Date: Tue, 6 Aug 2024 11:04:37 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20=EB=A6=AC=EB=B7=B0=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20query=20=ED=9B=85=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20HTTP=20=EC=9A=94=EC=B2=AD=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89=20=20(#216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: DetailedPage/index.tsx 리팩토링 - early return를 사용해 코드의 가독성을 높임 * feat: useGetDetailedReview 훅 생성 및 DetailedReviewPageContents에 적용 * feat: useSearchParamAndQuery 훅 생성 및 DetailedPageContent에 적용 * refactor: 리뷰 상세페이지에서 id라고 사용했던 key값, params의 key를 reviewId로 변경 - DetailedReview의 router param을 id에서 reviewId로 변경 * ci: dependencies에서 jest 삭제 및 ts-jest 설치 * ci: jest에서 절대 경로 사용할 수 있도록 jest.config.js 추가 * chore: eslint적용 제외 파일에 jest.config.js, tsconfig.json 추가 * ci: jest의 testEnvioronment를 jsdom으로 설정 * refactor: useGetDetailedReview에서 query 결과를 모두 반환하는 방식으로 변경 * fix: jest에서 msw ver2를 목서버로 사용 시 생기는 오류 수정 1. msw/node 를 읽지 못함 - jest.config.js의 testEnvironment 빈문자열 2. ReferentError: TextEnCoder is not defined - 해결 : jest.polyfills.js 추가 및 undici 설치 3. ReferenceError: ReadableStream is not defined - 해결 : undici 다운 그레이드 undici": "^6.19.5", -> "^5.0.0" * ci : jest에서 env 파일 읽을 수 있도록 dotenv 설치 및 jest에 적용 * fix: mock 핸들러인 getDetailedReview 에서 중복된 쿼리 매개 변수 사용 수정 - 오류 상황: jest에서 msw 사용 시, get의 url에 파라미터 사용 시 중복된 쿼리 매개 변수 오류가 남 - 오류 메세지 ::Found a redundant usage of query parameters in the request handler - 해결: 리뷰 상세보기 페이지의 reviews까지의 url 상수를 만들고, get에서는 이 상수를 활용한 정규표현식으로 리뷰 상세보기 페이지로 오는 모든 요청을 가로챌 수 있도록 함 * refactor: getWrongDetailedReview 목서버 핸들러 및 관련 상수 삭제 - getDetailedReview에서 request를 분석해 http오류 여부를 결정함 * feat: queryClientWrapper 생성 - queryClientWrapper : msw를 사용한 jest 테스트에 queryWrapper로 사용 * test:리뷰 상세 페이지 api 요청 성공에 대한 테스트 추가 * fix: groupAccessCodeAtom의 기본값 원래대로 복구 * chore:queryClientWrapper 네이밍 표기법을 파스칼 케이스로 변경 * fix: 머지 충돌 방지를 위해 yarn.lock 삭제 * fix: 머지 시 yarn.lock 충돌 해결 --- frontend/src/apis/endpoints.ts | 4 +- frontend/src/hooks/index.ts | 2 + .../review/useGetDetailedReview/index.ts | 33 +++++++++++++++++ .../hooks/review/useGetDetailedReview/test.ts | 26 +++++++++++++ frontend/src/hooks/useSearchParamAndQuery.ts | 22 +++++++++++ frontend/src/index.tsx | 2 +- frontend/src/mocks/handlers/review.ts | 37 ++++++++----------- .../mocks/mockData/detailedReviewMockData.ts | 5 --- .../DetailedReviewPageContents/index.tsx | 30 +++++---------- .../src/pages/DetailedReviewPage/index.tsx | 14 +++---- .../src/queryTestSetup/QueryClientWrapper.tsx | 11 ++++++ 11 files changed, 127 insertions(+), 59 deletions(-) create mode 100644 frontend/src/hooks/review/useGetDetailedReview/index.ts create mode 100644 frontend/src/hooks/review/useGetDetailedReview/test.ts create mode 100644 frontend/src/hooks/useSearchParamAndQuery.ts create mode 100644 frontend/src/queryTestSetup/QueryClientWrapper.tsx 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;