Skip to content

Commit

Permalink
[FE] refactor: LandingPage에 리액트 쿼리 적용 및 리팩토링 (#218)
Browse files Browse the repository at this point in the history
* chore: LandingPage의 styles 파일 분리

* fix: POST 요청을 하는 함수의 이름을 post~로 수정

* feat: 그룹 데이터 생성 요청에 대한 MSW 핸들러 추가

* refactor: 모킹 데이터 값을 더 직관적으로 수정

* refactor: LandingPage를 ErrorSuspenseContainer가 감싸도록 수정

* refactor: URL을 얻어오는 API에 react-query 적용 및 API 호출 함수 이름 수정

* chore: LandingPage 하위 컴포넌트들의 index 파일 추가 및 적용

* refactor: groupAccessCode 관련 msw 핸들러 추가 및 에러 상태(없는 코드 입력, 서버 에러)에 따른 에러 메세지를 출력하도록 수정

* refactor: groupAccessCode에 알파벳 대소문자와 숫자만 올 수 있도록 수정

* refactor: LandingPage에서 ErrorSuspenseContainer를 제거하고 대신 URLGeneratorForm만을 감싸도록 수정

* refactor: Input 컴포넌트의 onChange 이벤트 타입 수정

* refactor: Input 컴포넌트에 name 속성 추가

* refactor: 수정된 경로 반영

* refactor: usePostDataForUrl 쿼리에서 mutation을 리턴하도록 수정

* refactor: URL을 성공적으로 생성한 이후 Input을 리셋하는 함수 추가

* chore: NOTE 주석 추가

* refactor: getIsValidGroupAccessCodeApi에서 400 외의 에러 처리를 기존의 createApiErrorMessage를 사용하도록 수정

* chore: 누락됐던 -Api suffix 반영
  • Loading branch information
ImxYJL authored Aug 7, 2024
1 parent 19b392b commit 43f6b82
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 119 deletions.
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 postDataForURLApi = 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));

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

0 comments on commit 43f6b82

Please sign in to comment.