From 82cd7b0ab9b3070ea276cc4df8087e7ba58507d7 Mon Sep 17 00:00:00 2001 From: Fe <64690761+chysis@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:48:47 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20=EB=A6=AC=EB=B7=B0=20=EB=AA=A8=EC=95=84?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=97=B0=EB=8F=99=20(#817)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 섹션 리스트 요청 엔드포인트 및 호출 로직 작성 * feat: react-query를 사용하여 섹션 리스트 요청 로직 구현 및 msw 모킹 * feat: 모아보기 리뷰 엔드포인트 및 호출 로직 작성 * fix: Dropdown 컴포넌트가 상태로 DropdownItem을 가지도록 수정 * feat: react-query를 사용하여 섹션별 모아보기 요청 로직 구현 및 msw 모킹 * fix: msw handler에 쿠키 확인하는 코드 추가 * feat: 형광펜 에디터 적용 및 인터페이스 수정 * chore: 불필요한 코드 제거 --- frontend/src/apis/endpoints.ts | 2 + frontend/src/apis/review.ts | 48 ++++++++++++++++++- .../src/components/common/Dropdown/index.tsx | 14 +++--- frontend/src/constants/queryKey.ts | 2 + frontend/src/hooks/useDropdown.ts | 6 ++- frontend/src/mocks/handlers/review.ts | 21 +++++++- .../src/mocks/mockData/reviewCollection.ts | 10 ++++ .../hooks/useGetGroupedReviews.ts | 26 ++++++++++ .../hooks/useGetSectionList.ts | 22 +++++++++ .../src/pages/ReviewCollectionPage/index.tsx | 31 ++++++------ frontend/src/types/review.ts | 5 +- 11 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts create mode 100644 frontend/src/pages/ReviewCollectionPage/hooks/useGetSectionList.ts diff --git a/frontend/src/apis/endpoints.ts b/frontend/src/apis/endpoints.ts index 6c85d99dd..234e90667 100644 --- a/frontend/src/apis/endpoints.ts +++ b/frontend/src/apis/endpoints.ts @@ -69,6 +69,8 @@ const endPoint = { checkingPassword: `${serverUrl}/${VERSION2}/${REVIEW_PASSWORD_API_PARAMS.resource}/${REVIEW_PASSWORD_API_PARAMS.queryString.check}`, gettingReviewGroupData: (reviewRequestCode: string) => `${REVIEW_GROUP_DATA_API_URL}?${REVIEW_GROUP_DATA_API_PARAMS.queryString.reviewRequestCode}=${reviewRequestCode}`, + gettingSectionList: `${serverUrl}/${VERSION2}/sections`, + gettingGroupedReviews: (sectionId: number) => `${serverUrl}/${VERSION2}/reviews/gather?sectionId=${sectionId}`, postingHighlight: `${serverUrl}/${VERSION2}/highlight`, }; diff --git a/frontend/src/apis/review.ts b/frontend/src/apis/review.ts index df72b4709..6869ef348 100644 --- a/frontend/src/apis/review.ts +++ b/frontend/src/apis/review.ts @@ -1,4 +1,12 @@ -import { DetailReviewData, ReviewList, ReviewWritingFormResult, ReviewWritingFormData, ReviewInfoData } from '@/types'; +import { + DetailReviewData, + ReviewList, + ReviewWritingFormResult, + ReviewWritingFormData, + GroupedSection, + GroupedReviews, + ReviewInfoData, +} from '@/types'; import createApiErrorMessage from './apiErrorMessageCreator'; import endPoint from './endpoints'; @@ -92,3 +100,41 @@ export const getReviewListApi = async ({ lastReviewId, size }: GetReviewListApi) const data = await response.json(); return data as ReviewList; }; + +export const getSectionList = async () => { + const response = await fetch(endPoint.gettingSectionList, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(createApiErrorMessage(response.status)); + } + + const data = await response.json(); + return data as GroupedSection; +}; + +interface GetGroupedReviewsProps { + sectionId: number; +} + +export const getGroupedReviews = async ({ sectionId }: GetGroupedReviewsProps) => { + const response = await fetch(endPoint.gettingGroupedReviews(sectionId), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(createApiErrorMessage(response.status)); + } + + const data = await response.json(); + return data as GroupedReviews; +}; diff --git a/frontend/src/components/common/Dropdown/index.tsx b/frontend/src/components/common/Dropdown/index.tsx index 8943573f9..9ae816c14 100644 --- a/frontend/src/components/common/Dropdown/index.tsx +++ b/frontend/src/components/common/Dropdown/index.tsx @@ -3,31 +3,31 @@ import useDropdown from '@/hooks/useDropdown'; import * as S from './styles'; -interface DropdownItem { +export interface DropdownItem { text: string; - value: string; + value: string | number; } interface DropdownProps { items: DropdownItem[]; - selectedItem: string; - handleSelect: (item: string) => void; + selectedItem: DropdownItem; + handleSelect: (item: DropdownItem) => void; } -const Dropdown = ({ items, selectedItem: selectedOption, handleSelect }: DropdownProps) => { +const Dropdown = ({ items, selectedItem, handleSelect }: DropdownProps) => { const { isOpened, handleDropdownButtonClick, handleOptionClick, dropdownRef } = useDropdown({ handleSelect }); return ( - {selectedOption} + {selectedItem.text} {isOpened && ( {items.map((item) => { return ( - handleOptionClick(item.value)}> + handleOptionClick(item)}> {item.text} ); diff --git a/frontend/src/constants/queryKey.ts b/frontend/src/constants/queryKey.ts index f4f89e6fb..452f55f2b 100644 --- a/frontend/src/constants/queryKey.ts +++ b/frontend/src/constants/queryKey.ts @@ -3,6 +3,8 @@ export const REVIEW_QUERY_KEY = { reviews: 'reviews', writingReviewInfo: 'writingReviewInfo', postReview: 'postReview', + sectionList: 'sectionList', + groupedReviews: 'groupedReviews', reviewInfoData: 'reviewInfoData', }; diff --git a/frontend/src/hooks/useDropdown.ts b/frontend/src/hooks/useDropdown.ts index da17270ca..f73aeaa82 100644 --- a/frontend/src/hooks/useDropdown.ts +++ b/frontend/src/hooks/useDropdown.ts @@ -1,7 +1,9 @@ import { useEffect, useRef, useState } from 'react'; +import { DropdownItem } from '@/components/common/Dropdown'; + interface UseDropdownProps { - handleSelect: (option: string) => void; + handleSelect: (option: DropdownItem) => void; } const useDropdown = ({ handleSelect }: UseDropdownProps) => { @@ -13,7 +15,7 @@ const useDropdown = ({ handleSelect }: UseDropdownProps) => { setIsOpened((prev) => !prev); }; - const handleOptionClick = (option: string) => { + const handleOptionClick = (option: DropdownItem) => { handleSelect(option); setIsOpened(false); }; diff --git a/frontend/src/mocks/handlers/review.ts b/frontend/src/mocks/handlers/review.ts index 43b6bcb2a..678b6d711 100644 --- a/frontend/src/mocks/handlers/review.ts +++ b/frontend/src/mocks/handlers/review.ts @@ -17,6 +17,7 @@ import { MOCK_AUTH_TOKEN_NAME, MOCK_REVIEW_INFO_DATA, } from '../mockData'; +import { GROUPED_REVIEWS_MOCK_DATA, GROUPED_SECTION_MOCK_DATA } from '../mockData/reviewCollection'; export const PAGE = { firstPageNumber: 1, @@ -104,12 +105,30 @@ const postReview = () => return HttpResponse.json({ message: 'post 성공' }, { status: 201 }); }); +const getSectionList = () => + http.get(endPoint.gettingSectionList, async ({ request, cookies }) => { + // authToken 쿠키 확인 + if (!cookies[MOCK_AUTH_TOKEN_NAME]) return HttpResponse.json({ error: '인증 관련 쿠키 없음' }, { status: 401 }); + + return HttpResponse.json(GROUPED_SECTION_MOCK_DATA); + }); + +const getGroupedReviews = (sectionId: number) => + http.get(endPoint.gettingGroupedReviews(sectionId), async ({ request, cookies }) => { + // authToken 쿠키 확인 + if (!cookies[MOCK_AUTH_TOKEN_NAME]) return HttpResponse.json({ error: '인증 관련 쿠키 없음' }, { status: 401 }); + + return HttpResponse.json(GROUPED_REVIEWS_MOCK_DATA); + }); + const reviewHandler = [ getDetailedReview(), getReviewList(null, 10), getDataToWriteReview(), - postReview(), + getSectionList(), + getGroupedReviews(1), getReviewInfoData(), + postReview(), ]; export default reviewHandler; diff --git a/frontend/src/mocks/mockData/reviewCollection.ts b/frontend/src/mocks/mockData/reviewCollection.ts index 94aa20786..d7ee7b730 100644 --- a/frontend/src/mocks/mockData/reviewCollection.ts +++ b/frontend/src/mocks/mockData/reviewCollection.ts @@ -23,6 +23,7 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = { reviews: [ { question: { + id: 1, name: '커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요', type: 'CHECKBOX', }, @@ -39,25 +40,34 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = { }, { question: { + id: 2, name: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요', type: 'TEXT', }, answers: [ { + id: 1, content: '장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.', + highlights: [], }, { + id: 2, content: '고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다.', + highlights: [], }, { + id: 3, content: '장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.', + highlights: [], }, { + id: 4, content: '고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다.', + highlights: [], }, ], votes: null, diff --git a/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts b/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts new file mode 100644 index 000000000..be16a1427 --- /dev/null +++ b/frontend/src/pages/ReviewCollectionPage/hooks/useGetGroupedReviews.ts @@ -0,0 +1,26 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getGroupedReviews } from '@/apis/review'; +import { REVIEW_QUERY_KEY } from '@/constants'; +import { GroupedReviews } from '@/types'; + +interface UseGetGroupedReviewsProps { + sectionId: number; +} + +const useGetGroupedReviews = ({ sectionId }: UseGetGroupedReviewsProps) => { + const fetchGroupedReviews = async () => { + const result = await getGroupedReviews({ sectionId }); + return result; + }; + + const result = useSuspenseQuery({ + queryKey: [REVIEW_QUERY_KEY.groupedReviews, sectionId], + queryFn: () => fetchGroupedReviews(), + staleTime: 1 * 60 * 1000, + }); + + return result; +}; + +export default useGetGroupedReviews; diff --git a/frontend/src/pages/ReviewCollectionPage/hooks/useGetSectionList.ts b/frontend/src/pages/ReviewCollectionPage/hooks/useGetSectionList.ts new file mode 100644 index 000000000..1e094d068 --- /dev/null +++ b/frontend/src/pages/ReviewCollectionPage/hooks/useGetSectionList.ts @@ -0,0 +1,22 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getSectionList } from '@/apis/review'; +import { REVIEW_QUERY_KEY } from '@/constants'; +import { GroupedSection } from '@/types'; + +const useGetSectionList = () => { + const fetchSectionList = async () => { + const result = await getSectionList(); + return result; + }; + + const result = useSuspenseQuery({ + queryKey: [REVIEW_QUERY_KEY.sectionList], + queryFn: () => fetchSectionList(), + staleTime: 60 * 60 * 1000, + }); + + return result; +}; + +export default useGetSectionList; diff --git a/frontend/src/pages/ReviewCollectionPage/index.tsx b/frontend/src/pages/ReviewCollectionPage/index.tsx index a4fc17672..1c3589378 100644 --- a/frontend/src/pages/ReviewCollectionPage/index.tsx +++ b/frontend/src/pages/ReviewCollectionPage/index.tsx @@ -1,23 +1,23 @@ import { useState } from 'react'; import { Accordion, AuthAndServerErrorFallback, Dropdown, ErrorSuspenseContainer, TopButton } from '@/components'; +import { DropdownItem } from '@/components/common/Dropdown'; +import HighlightEditor from '@/components/highlight/HighlightEditor'; import ReviewDisplayLayout from '@/components/layouts/ReviewDisplayLayout'; -import { useGetReviewList } from '@/hooks'; -import { GROUPED_REVIEWS_MOCK_DATA, GROUPED_SECTION_MOCK_DATA } from '@/mocks/mockData/reviewCollection'; import DoughnutChart from './components/DoughnutChart'; +import useGetGroupedReviews from './hooks/useGetGroupedReviews'; +import useGetSectionList from './hooks/useGetSectionList'; import * as S from './styles'; const ReviewCollectionPage = () => { - // TODO: 추후 리뷰 그룹 정보를 받아오는 API로 대체 - const { data } = useGetReviewList(); - const { revieweeName, projectName } = data.pages[0]; - - // TODO: react-query 적용 및 드롭다운 아이템 선택 시 요청 - const reviewSectionList = GROUPED_SECTION_MOCK_DATA.sections.map((section) => { - return { text: section.name, value: section.name }; + const { data: reviewSectionList } = useGetSectionList(); + const dropdownSectionList = reviewSectionList.sections.map((section) => { + return { text: section.name, value: section.id }; }); - const [reviewSection, setReviewSection] = useState(reviewSectionList[0].value); + + const [selectedSection, setSelectedSection] = useState(dropdownSectionList[0]); + const { data: groupedReviews } = useGetGroupedReviews({ sectionId: selectedSection.value as number }); return ( @@ -25,19 +25,22 @@ const ReviewCollectionPage = () => { setReviewSection(item)} + items={dropdownSectionList} + selectedItem={dropdownSectionList.find((section) => section.value === selectedSection.value)!} + handleSelect={(item) => setSelectedSection(item)} /> - {GROUPED_REVIEWS_MOCK_DATA.reviews.map((review, index) => { + {groupedReviews.reviews.map((review, index) => { return ( {review.question.type === 'CHECKBOX' ? ( ) : ( + {review.answers && ( + + )} {review.answers?.map((answer, index) => { return {answer.content}; })} diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts index 00f3a51f3..041d4d731 100644 --- a/frontend/src/types/review.ts +++ b/frontend/src/types/review.ts @@ -1,3 +1,5 @@ +import { ReviewAnswerResponseData } from './highlight'; + export interface Keyword { id: number; content: string; @@ -110,6 +112,7 @@ export interface GroupedReviews { export interface GroupedReview { question: { + id: number; name: string; type: QuestionType; }; @@ -117,7 +120,7 @@ export interface GroupedReview { * CollectedReviewAnswer[] : 주관식 질문에서 답변 모아놓은 배열 * null : 객관식 질문인 경우 */ - answers: ReviewAnswer[] | null; + answers: ReviewAnswerResponseData[] | null; /** * CollectedReviewVotes[] : 객관식 질문에서 옵션-득표수 모아놓은 배열 * null : 주관식 질문인 경우