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] refactor: LandingPage에 리액트 쿼리 적용 및 리팩토링 #218

Merged
merged 20 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bbdb7d2
chore: LandingPage의 styles 파일 분리
ImxYJL Aug 3, 2024
bf4f592
fix: POST 요청을 하는 함수의 이름을 post~로 수정
ImxYJL Aug 3, 2024
d9fef29
feat: 그룹 데이터 생성 요청에 대한 MSW 핸들러 추가
ImxYJL Aug 3, 2024
8df4ef3
refactor: 모킹 데이터 값을 더 직관적으로 수정
ImxYJL Aug 3, 2024
fec364e
refactor: LandingPage를 ErrorSuspenseContainer가 감싸도록 수정
ImxYJL Aug 4, 2024
269655e
refactor: URL을 얻어오는 API에 react-query 적용 및 API 호출 함수 이름 수정
ImxYJL Aug 4, 2024
bc2efd8
chore: LandingPage 하위 컴포넌트들의 index 파일 추가 및 적용
ImxYJL Aug 4, 2024
117cb0e
refactor: groupAccessCode 관련 msw 핸들러 추가 및 에러 상태(없는 코드 입력, 서버 에러)에 따른 …
ImxYJL Aug 4, 2024
b118388
refactor: groupAccessCode에 알파벳 대소문자와 숫자만 올 수 있도록 수정
ImxYJL Aug 4, 2024
4ade792
refactor: LandingPage에서 ErrorSuspenseContainer를 제거하고 대신 URLGeneratorF…
ImxYJL Aug 5, 2024
cca5eb7
refactor: Input 컴포넌트의 onChange 이벤트 타입 수정
ImxYJL Aug 5, 2024
3ef4d31
refactor: Input 컴포넌트에 name 속성 추가
ImxYJL Aug 5, 2024
c484cc7
Merge branch 'develop' of https://github.com/woowacourse-teams/2024-r…
ImxYJL Aug 6, 2024
24bb091
Merge branch 'develop' of https://github.com/woowacourse-teams/2024-r…
ImxYJL Aug 6, 2024
ed8f888
refactor: 수정된 경로 반영
ImxYJL Aug 6, 2024
79c6cc6
refactor: usePostDataForUrl 쿼리에서 mutation을 리턴하도록 수정
ImxYJL Aug 6, 2024
21df3c4
refactor: URL을 성공적으로 생성한 이후 Input을 리셋하는 함수 추가
ImxYJL Aug 6, 2024
dbd335a
chore: NOTE 주석 추가
ImxYJL Aug 6, 2024
8fe6414
refactor: getIsValidGroupAccessCodeApi에서 400 외의 에러 처리를 기존의 createApiE…
ImxYJL Aug 6, 2024
552cc6f
chore: 누락됐던 -Api suffix 반영
ImxYJL Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const endPoint = {
gettingDataToWriteReview: (reviewRequestCode: string) =>
`${process.env.API_BASE_URL}/reviews/write?${REVIEW_WRITING_API_PARAMS.queryString.reviewRequestCode}=${reviewRequestCode}`,
gettingReviewList: `${process.env.API_BASE_URL}/reviews`,
gettingCreatedGroupData: `${process.env.API_BASE_URL}/groups`,
postingDataForURL: `${process.env.API_BASE_URL}/groups`,
};

export default endPoint;
26 changes: 23 additions & 3 deletions frontend/src/apis/group.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { INVALID_GROUP_ACCESS_CODE_MESSAGE } from '@/constants';

import createApiErrorMessage from './apiErrorMessageCreator';
import endPoint from './endpoints';

interface DataForURL {
export interface DataForURL {
revieweeName: string;
projectName: string;
}

export const getCreatedGroupDataApi = async (dataForURL: DataForURL) => {
const response = await fetch(endPoint.gettingCreatedGroupData, {
export const postDataForURL = async (dataForURL: DataForURL) => {
const response = await fetch(endPoint.postingDataForURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -22,3 +24,21 @@ export const getCreatedGroupDataApi = async (dataForURL: DataForURL) => {
const data = await response.json();
return data;
};

// NOTE: 리뷰 목록 엔드포인트(gettingReviewList)에 요청을 보내고 있지만,
// 요청 성격이 목록을 얻어오는 것이 아닌 유효한 groupAccessCode인지 확인하는 것이므로 group 파일에 작성함
// 단, 해당 엔드포인트에 대한 정상 요청 핸들러가 동작한다면 아래 에러 핸들러는 동작하지 않음
export const getIsValidGroupAccessCodeApi = async (groupAccessCode: string) => {
const response = await fetch(endPoint.gettingReviewList, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
GroupAccessCode: groupAccessCode,
},
});

if (response.status === 400) throw new Error(INVALID_GROUP_ACCESS_CODE_MESSAGE);
if (!response.ok) throw new Error(createApiErrorMessage(response.status));
Comment on lines +40 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400 에러도 createApiErrorMessage(response.status) 로 잡지 않은 이유가 특이 케이스여서 군요!


return response.ok;
};
12 changes: 0 additions & 12 deletions frontend/src/apis/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,3 @@ export const getReviewListApi = async (groupAccessCode: string) => {
const data = await response.json();
return data as ReviewList;
};

export const checkGroupAccessCodeApi = async (groupAccessCode: string) => {
const response = await fetch(endPoint.gettingReviewList, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
GroupAccessCode: groupAccessCode,
},
});

return response.ok;
};
19 changes: 12 additions & 7 deletions frontend/src/components/common/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ export interface InputStyleProps {
}
interface InputProps extends InputStyleProps {
value: string;
onChange: (value: string) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
type: string;
id?: string;
name?: string;
placeholder?: string;
}

const Input = ({ id, value, onChange, type, placeholder, $style }: InputProps) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};

const Input = ({ id, value, name, onChange, type, placeholder, $style }: InputProps) => {
return (
<S.Input id={id} value={value} type={type} onChange={handleChange} placeholder={placeholder} $style={$style} />
<S.Input
id={id}
value={value}
type={type}
name={name}
onChange={onChange}
placeholder={placeholder}
style={$style}
/>
);
};

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/constants/errorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const API_ERROR_MESSAGE: ApiErrorMessages = {
export const SERVER_ERROR_REGEX = /^5\d{2}$/;

export const ROUTE_ERROR_MESSAGE = '찾으시는 페이지가 없어요.';

export const INVALID_GROUP_ACCESS_CODE_MESSAGE = '올바르지 않은 확인 코드예요.';
5 changes: 5 additions & 0 deletions frontend/src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// TODO: 내용이 배열이 아니므로 단수형으로 수정하기
export const REVIEW_QUERY_KEYS = {
detailedReview: 'detailedReview',
reviews: 'reviews',
};

export const GROUP_QUERY_KEY = {
dataForURL: 'dataForURL',
};
43 changes: 43 additions & 0 deletions frontend/src/mocks/handlers/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { http, HttpResponse } from 'msw';

import endPoint from '@/apis/endpoints';

import { CREATED_GROUP_DATA, INVALID_GROUP_ACCESS_CODE } from '../mockData/group';

// NOTE: URL 생성 정상 응답
const postDataForUrl = () => {
return http.post(endPoint.postingDataForURL, async () => {
return HttpResponse.json(CREATED_GROUP_DATA, { status: 200 });
});
};

// NOTE: URL 생성 에러 응답
// const postDataForUrl = () => {
// return http.post(endPoint.postingDataForURL, async () => {
// return HttpResponse.json({ error: '서버 에러 테스트' }, { status: 500 });
// });
// };

// NOTE: 확인 코드 정상 응답
const getIsValidGroupAccessCode = () => {
return http.get(endPoint.gettingReviewList, async () => {
return HttpResponse.json({ status: 200 });
});
};

// NOTE: 확인 코드 에러 응답
// const getIsValidGroupAccessCode = () => {
// return http.get(endPoint.gettingReviewList, async () => {
// return HttpResponse.json(INVALID_GROUP_ACCESS_CODE, { status: 400 });
// });
// };

// const getIsValidGroupAccessCode = () => {
// return http.get(endPoint.gettingReviewList, async () => {
// return HttpResponse.json({ error: '서버 에러 테스트' }, { status: 500 });
// });
// };

const groupHandler = [postDataForUrl(), getIsValidGroupAccessCode()];

export default groupHandler;
3 changes: 2 additions & 1 deletion frontend/src/mocks/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import groupHandler from './group';
import reviewHandler from './review';

const handlers = [...reviewHandler];
const handlers = [...reviewHandler, ...groupHandler];

export default handlers;
12 changes: 12 additions & 0 deletions frontend/src/mocks/mockData/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const CREATED_GROUP_DATA = {
reviewRequestCode: 'mocked-reviewRequestCode',
groupAccessCode: 'mocked-groupAccessCode',
};

export const INVALID_GROUP_ACCESS_CODE = {
type: 'about:blank',
title: 'Bad Request',
status: 400,
detail: '올바르지 않은 확인 코드입니다.',
instance: '/reviews',
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import { EssentialPropsWithChildren } from '@/types';

import * as S from '../../styles';
import * as S from './styles';

interface FormBodyProps {
direction: React.CSSProperties['flexDirection'];
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/pages/LandingPage/components/FormBody/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from '@emotion/styled';

export const FormBody = styled.div<{ direction: React.CSSProperties['flexDirection'] }>`
display: flex;
flex-direction: ${({ direction }) => direction};
gap: 1.6em;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React from 'react';

import { EssentialPropsWithChildren } from '@/types';

import * as S from '../../styles';
import FormBody from '../FormBody';
import { FormBody } from '../index';

import * as S from './styles';

interface FormProps {
title: string;
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/pages/LandingPage/components/FormLayout/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from '@emotion/styled';

export const FormLayout = styled.form`
display: flex;
flex-direction: column;

width: 40rem;
`;

export const Title = styled.h2`
font-size: ${({ theme }) => theme.fontSize.basic};

margin-bottom: 2.2rem;
`;
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';

import { checkGroupAccessCodeApi } from '@/apis/review';
import { getIsValidGroupAccessCodeApi } from '@/apis/group';
import { Input, Button } from '@/components';
import { useGroupAccessCode } from '@/hooks';
import { debounce } from '@/utils/debounce';

import * as S from '../../styles';
import FormLayout from '../FormLayout';
import { FormLayout } from '../index';

import * as S from './styles';

const DEBOUNCE_TIME = 300;

// NOTE: groupAccessCode가 유효한지를 확인하는 API 호출은 fetch로 고정!
// 1. 요청을 통해 단순히 true, false 정도의 데이터를 단발적으로 가져오는 API이므로
// 리액트 쿼리를 사용할 만큼 서버 상태를 정교하게 가지고 있을 필요 없음
// 2. 리액트 쿼리를 도입했을 때 Errorboundary로 Form을 감싸지 않았고, useQuery를 사용했음에도 불구하고
// error fallback이 뜨는 버그 존재
const ReviewAccessForm = () => {
const navigate = useNavigate();
const { updateGroupAccessCode } = useGroupAccessCode();

const [groupAccessCode, setGroupAccessCode] = useState('');
const [errorMessage, setErrorMessage] = useState('');

const navigate = useNavigate();
const { updateGroupAccessCode } = useGroupAccessCode();

const isValidGroupAccessCode = async () => {
const isValid = await checkGroupAccessCodeApi(groupAccessCode);
const isValid = await getIsValidGroupAccessCodeApi(groupAccessCode);
return isValid;
};

const handleGroupAccessCodeInputChange = (value: string) => {
setGroupAccessCode(value);
const isAlphanumeric = (groupAccessCode: string) => {
const alphanumericRegex = /^[A-Za-z0-9]*$/;
return alphanumericRegex.test(groupAccessCode);
};

const handleGroupAccessCodeInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setGroupAccessCode(event.target.value);
};

const handleAccessReviewButtonClick = debounce(async (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();

try {
const isValid = await isValidGroupAccessCode();

if (isValid) {
updateGroupAccessCode(groupAccessCode);
setErrorMessage('');

navigate('/user/review-preview-list');
} else {
setErrorMessage('유효하지 않은 그룹 접근 코드입니다.');
if (!isAlphanumeric(groupAccessCode)) {
setErrorMessage('알파벳 대소문자와 숫자만 입력 가능합니다.');
return;
}

await isValidGroupAccessCode();

updateGroupAccessCode(groupAccessCode);
setErrorMessage('');

navigate('/user/review-list');
} catch (error) {
setErrorMessage('오류가 발생했습니다. 다시 시도해주세요.');
if (error instanceof Error) setErrorMessage(error.message);
}
}, DEBOUNCE_TIME);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import styled from '@emotion/styled';

export const ReviewAccessFormContent = styled.div`
display: flex;
flex-direction: column;

width: 100%;
`;

export const ReviewAccessFormBody = styled.div`
display: flex;
justify-content: space-between;

width: 100%;
`;

export const ErrorMessage = styled.p`
font-size: 1.3rem;

color: ${({ theme }) => theme.colors.red};

padding-left: 0.7rem;
`;
Loading