Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 리뷰 모아보기 페이지 API 로직 연동 #817

Merged
merged 10 commits into from
Oct 10, 2024
Merged
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