Skip to content

Commit

Permalink
[FE] 리뷰 모아보기 페이지 API 로직 연동 (#817)
Browse files Browse the repository at this point in the history
* feat: 섹션 리스트 요청 엔드포인트 및 호출 로직 작성

* feat: react-query를 사용하여 섹션 리스트 요청 로직 구현 및 msw 모킹

* feat: 모아보기 리뷰 엔드포인트 및 호출 로직 작성

* fix: Dropdown 컴포넌트가 상태로 DropdownItem을 가지도록 수정

* feat: react-query를 사용하여 섹션별 모아보기 요청 로직 구현 및 msw 모킹

* fix: msw handler에 쿠키 확인하는 코드 추가

* feat: 형광펜 에디터 적용 및 인터페이스 수정

* chore: 불필요한 코드 제거
  • Loading branch information
chysis authored and skylar1220 committed Nov 5, 2024
1 parent 7f02905 commit 82cd7b0
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 26 deletions.
2 changes: 2 additions & 0 deletions frontend/src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
};

Expand Down
48 changes: 47 additions & 1 deletion frontend/src/apis/review.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
};
14 changes: 7 additions & 7 deletions frontend/src/components/common/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<S.DropdownContainer ref={dropdownRef}>
<S.DropdownButton onClick={handleDropdownButtonClick}>
<S.SelectedOption>{selectedOption}</S.SelectedOption>
<S.SelectedOption>{selectedItem.text}</S.SelectedOption>
<S.ArrowIcon src={DownArrowIcon} $isOpened={isOpened} alt="" />
</S.DropdownButton>
{isOpened && (
<S.ItemContainer>
{items.map((item) => {
return (
<S.DropdownItem key={item.value} onClick={() => handleOptionClick(item.value)}>
<S.DropdownItem key={item.value} onClick={() => handleOptionClick(item)}>
{item.text}
</S.DropdownItem>
);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export const REVIEW_QUERY_KEY = {
reviews: 'reviews',
writingReviewInfo: 'writingReviewInfo',
postReview: 'postReview',
sectionList: 'sectionList',
groupedReviews: 'groupedReviews',
reviewInfoData: 'reviewInfoData',
};

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/hooks/useDropdown.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -13,7 +15,7 @@ const useDropdown = ({ handleSelect }: UseDropdownProps) => {
setIsOpened((prev) => !prev);
};

const handleOptionClick = (option: string) => {
const handleOptionClick = (option: DropdownItem) => {
handleSelect(option);
setIsOpened(false);
};
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/mocks/handlers/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
10 changes: 10 additions & 0 deletions frontend/src/mocks/mockData/reviewCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = {
reviews: [
{
question: {
id: 1,
name: '커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요',
type: 'CHECKBOX',
},
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GroupedReviews>({
queryKey: [REVIEW_QUERY_KEY.groupedReviews, sectionId],
queryFn: () => fetchGroupedReviews(),
staleTime: 1 * 60 * 1000,
});

return result;
};

export default useGetGroupedReviews;
22 changes: 22 additions & 0 deletions frontend/src/pages/ReviewCollectionPage/hooks/useGetSectionList.ts
Original file line number Diff line number Diff line change
@@ -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<GroupedSection>({
queryKey: [REVIEW_QUERY_KEY.sectionList],
queryFn: () => fetchSectionList(),
staleTime: 60 * 60 * 1000,
});

return result;
};

export default useGetSectionList;
31 changes: 17 additions & 14 deletions frontend/src/pages/ReviewCollectionPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
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<DropdownItem>(dropdownSectionList[0]);
const { data: groupedReviews } = useGetGroupedReviews({ sectionId: selectedSection.value as number });

return (
<ErrorSuspenseContainer fallback={AuthAndServerErrorFallback}>
<ReviewDisplayLayout isReviewList={false}>
<S.ReviewCollectionContainer>
<S.ReviewSectionDropdown>
<Dropdown
items={reviewSectionList}
selectedItem={reviewSection}
handleSelect={(item) => setReviewSection(item)}
items={dropdownSectionList}
selectedItem={dropdownSectionList.find((section) => section.value === selectedSection.value)!}
handleSelect={(item) => setSelectedSection(item)}
/>
</S.ReviewSectionDropdown>
<S.ReviewCollection>
{GROUPED_REVIEWS_MOCK_DATA.reviews.map((review, index) => {
{groupedReviews.reviews.map((review, index) => {
return (
<Accordion title={review.question.name} key={index} isInitiallyOpened={index === 0 ? true : false}>
{review.question.type === 'CHECKBOX' ? (
<DoughnutChart reviewVotes={review.votes!} />
) : (
<S.ReviewAnswerContainer>
{review.answers && (
<HighlightEditor questionId={review.question.id} answerList={review.answers} />
)}
{review.answers?.map((answer, index) => {
return <S.ReviewAnswer key={index}>{answer.content}</S.ReviewAnswer>;
})}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/types/review.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReviewAnswerResponseData } from './highlight';

export interface Keyword {
id: number;
content: string;
Expand Down Expand Up @@ -110,14 +112,15 @@ export interface GroupedReviews {

export interface GroupedReview {
question: {
id: number;
name: string;
type: QuestionType;
};
/**
* CollectedReviewAnswer[] : 주관식 질문에서 답변 모아놓은 배열
* null : 객관식 질문인 경우
*/
answers: ReviewAnswer[] | null;
answers: ReviewAnswerResponseData[] | null;
/**
* CollectedReviewVotes[] : 객관식 질문에서 옵션-득표수 모아놓은 배열
* null : 주관식 질문인 경우
Expand Down

0 comments on commit 82cd7b0

Please sign in to comment.