Skip to content

Commit

Permalink
feat: 에러바운더리를 이용한 에러핸들링 (#667)
Browse files Browse the repository at this point in the history
* chore: 리액트 헬멧 라이브러리 제거 (#658)

* chore: 리액트 헬멧 라이브러리 제거 (#658)

* refactor: 셀럽잇 추천 맛집 섹션 분리 (#658)

* refactor: 배너 섹션 분리 (#658)

* refactor: 카테고리 섹션 분리 (#658)

* refactor: 셀럽 베스트 섹션 분리 (#658)

* refactor: 지역맛집 섹션 분리 (#658)

* refactor: 메인페이지 리팩터링 (#658)

* refactor: msw handler 폴더명 수정 및 랜덤 응답 로직 구현

랜덤으로 성공과 실패 응답을 보낸다.

* chore: 에러핸들링 개발환경 구축

개발 모드에서 에러 오버레이 일시 중단

* refactor: 셀럽잇 추천 맛집 스켈레톤 컴포넌트 분리 (#661)

* feat: 에러바운더리 구현 (#661)

* style: 셀럽잇 추천맛집 서브 컴포넌트명 수정 및 에러바운더리 적용 (#661)

* style: 핸들러 명 수정에 대한 적용

* design: 셀럽잇 추천 맛집 음식점 카드 좋아요 기능 보이기 (#661)

* refactor: useToggleLikeNotUpdate 에러 분기

429, 401 에러 핸들링

* fix: ifram 안보이는 현상 처리

* fix: 에러 오버레이 안보이는 현상 처리
  • Loading branch information
shackstack authored Dec 29, 2023
1 parent fbcc768 commit 7ac6f37
Show file tree
Hide file tree
Showing 11 changed files with 1,458 additions and 2,632 deletions.
2 changes: 1 addition & 1 deletion frontend/.webpack/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ module.exports = (env, args) => {
new Dotenv({
path: path.resolve(__dirname, `../.${TARGET_ENV}.env`),
}),
new RefreshWebpackPlugin(),
new RefreshWebpackPlugin({}),
],
};
};
46 changes: 46 additions & 0 deletions frontend/src/components/@common/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, ReactElement, ReactNode } from 'react';

interface FallbackRenderProps {
resetErrorBoundary: () => void;
}
interface Props {
children: ReactNode;
fallbackRender: ({ resetErrorBoundary }: FallbackRenderProps) => ReactElement;
reset: () => void;
}

interface State {
hasError: boolean;
}

class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(): State {
return {
hasError: true,
};
}

resetErrorBoundary() {
const { reset } = this.props;
reset();
this.setState({ hasError: false });
}

render() {
const { hasError } = this.state;
const { children, fallbackRender } = this.props;

if (hasError) {
return fallbackRender({ resetErrorBoundary: () => this.resetErrorBoundary() });
}

return children;
}
}

export default ErrorBoundary;
2 changes: 1 addition & 1 deletion frontend/src/hooks/server/useToggleLikeNotUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const useToggleLikeNotUpdate = (restaurant: Restaurant) => {
query.queryKey[0] === 'restaurants' && query.queryKey[1]?.type !== 'wish-list',
});
},
});
});

const toggleRestaurantLike = () => {
toggleLike.mutate(restaurant.id);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/mocks/handler/detailPage/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { makeImage } from '~/mocks/utils';

import type { Celeb } from '~/@types/celeb.types';
import type { HyphenatedDate } from '~/@types/date.types';
import type { RestaurantData, RestaurantReview, VideoList } from '~/@types/api.types';
import type { RestaurantData, VideoList } from '~/@types/api.types';

export const DetailPageSuccessHandler = [
rest.get('/restaurants/:restaurantsId', (req, res, ctx) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/mocks/handler/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { DetailPageSuccessHandler } from '~/mocks/handler/detailPage/handler';
export { MainPageSuccessHandler } from '~/mocks/handler/mainPage/handler';
export { MainPageSuccessHandler } from '~/mocks/handler/mapPages/handler';
export { WishListPageSuccessHandler } from '~/mocks/handler/wishListPage/handler';
export { newMainPageHandler } from '~/mocks/handler/newMainPage/handler';
export { newMainPageHandler } from '~/mocks/handler/mainPage/handler';
156 changes: 17 additions & 139 deletions frontend/src/mocks/handler/mainPage/handler.ts
Original file line number Diff line number Diff line change
@@ -1,148 +1,26 @@
import { rest } from 'msw';
import restaurants from '~/mocks/data/restaurants';
import { recommendation } from '~/mocks/data/recommendation';
import { RECOMMENDED_REGION } from '~/constants/recommendedRegion';
import { getRandomNumber } from '~/utils/getRandomNumber';

import restaurants from '../../data/restaurants';
import celebs from '../../data/celebs';
import { profile } from '../../data/user';

import type { Celeb } from '~/@types/celeb.types';
import type { RestaurantData, RestaurantListData } from '~/@types/api.types';

export const MainPageSuccessHandler = [
rest.get('/restaurants', (req, res, ctx) => {
const pageSize = 18;

export const newMainPageHandler = [
rest.get('/address', (req, res, ctx) => {
const queryParams = req.url.searchParams;
const sort = queryParams.get('sort') || 'distance';
const page = Number(queryParams.get('page')) || 0;
const celebId = Number(queryParams.get('celebId')) || null;
const category = queryParams.get('category') || null;
const lowLatitude = Number(queryParams.get('lowLatitude'));
const highLatitude = Number(queryParams.get('highLatitude'));
const lowLongitude = Number(queryParams.get('lowLongitude'));
const highLongitude = Number(queryParams.get('highLongitude'));

const filteredRestaurants = restaurants.filter(({ celebs, category: restaurantCategory }) => {
const hasCelebId = celebId ? celebs.map(({ id }) => id).includes(celebId) : true;
const isMatchCategory = category ? category === restaurantCategory : true;
return hasCelebId && isMatchCategory;
});

const sortedRestaurants = filteredRestaurants
.filter(restaurant => {
return (
restaurant.lat >= lowLatitude &&
restaurant.lat <= highLatitude &&
restaurant.lng >= lowLongitude &&
restaurant.lng <= highLongitude
);
})
.sort((prev, current) => {
if (sort === 'like') return current.likeCount - prev.likeCount;
return prev.distance - current.distance;
});

function moveCelebToFrontById(celebs: Celeb[], targetId: number): Celeb[] {
const targetIndex = celebs.findIndex(celeb => celeb.id === targetId);

if (targetIndex === -1) return celebs;

const newArray = [...celebs];
const [movedCeleb] = newArray.splice(targetIndex, 1);
newArray.unshift(movedCeleb);

return newArray;
}

const content: RestaurantData[] = sortedRestaurants
.slice(page * pageSize, (page + 1) * pageSize)
.map(({ celebs, ...etc }) => {
const sortedCelebs: Celeb[] = moveCelebToFrontById(celebs, celebId);
return { celebs: sortedCelebs, ...etc };
});
const regionKey = queryParams.get('codes') as keyof typeof RECOMMENDED_REGION;
const regions = RECOMMENDED_REGION[regionKey].name;

const restaurantListData: RestaurantListData = {
content,
currentElementsCount: content.length,
currentPage: page,
pageSize,
totalElementsCount: sortedRestaurants.length,
totalPage: Math.ceil(sortedRestaurants.length / pageSize),
};

return res(ctx.status(200), ctx.json(restaurantListData));
}),

rest.get('/celebs', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(celebs));
}),

rest.get('/oauth/login/:oauthType', (req, res, ctx) => {
const code = req.url.searchParams.get('code') ?? null;

if (code === null) {
return res(ctx.status(401), ctx.json({ message: '인증되지 않은 code입니다.' }));
}

const currentDate = new Date();
const sixHoursInMilliseconds = 6 * 60 * 60 * 1000;
const expirationDate = new Date(currentDate.getTime() + sixHoursInMilliseconds);

return res(ctx.cookie('JSESSION', `${code}`, { expires: expirationDate }), ctx.status(200));
}),

rest.get('/oauth/logout/:oauthType', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

return res(ctx.status(200), ctx.cookie('JSESSION', '', { expires: new Date() }));
}),

rest.delete('/oauth/withdraw/:oauthType', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

// 회원 탈퇴 시에 쿠키 바로 만료
return res(ctx.status(204), ctx.cookie('JSESSION', '', { expires: new Date() }));
}),

rest.get('/members/my', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

// 쿠키 갱신
return res(ctx.status(200), ctx.json(profile));
}),

rest.post('/restaurants/:restaurantId/like', (req, res, ctx) => {
const { JSESSION } = req.cookies;
const { restaurantId } = req.params;

const restaurant = restaurants.find(restaurant => restaurant.id === Number(restaurantId));
restaurant.isLiked ? (restaurant['isLiked'] = false) : (restaurant['isLiked'] = true);

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

return res(ctx.status(200));
}),
];
const restaurantFilteredByRegion = restaurants.filter(({ roadAddress }) =>
regions.some(region => roadAddress.includes(region)),
);

export const MainPageErrorHandler = [
rest.post('/restaurants/:restaurantId/like', (req, res, ctx) => {
return res(ctx.status(401));
return res(ctx.status(200), ctx.json({ content: restaurantFilteredByRegion }));
}),

rest.delete('/oauth/withdraw/:oauthType', (req, res, ctx) => {
return res(ctx.status(401));
rest.get('/main-page/recommendation', (req, res, ctx) => {
const responses = [res(ctx.status(401)), res(ctx.status(401)), res(ctx.status(200), ctx.json(recommendation))];
return responses[getRandomNumber()];
// return res(ctx.status(400), ctx.json(recommendation));
// return res(ctx.status(200), ctx.json(recommendation));
}),
];
152 changes: 152 additions & 0 deletions frontend/src/mocks/handler/mapPages/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { rest } from 'msw';

import restaurants from '../../data/restaurants';
import celebs from '../../data/celebs';
import { profile } from '../../data/user';

import type { Celeb } from '~/@types/celeb.types';
import type { RestaurantData, RestaurantListData } from '~/@types/api.types';
import { getRandomNumber } from '~/utils/getRandomNumber';

export const MainPageSuccessHandler = [
rest.get('/restaurants', (req, res, ctx) => {
const pageSize = 18;

const queryParams = req.url.searchParams;
const sort = queryParams.get('sort') || 'distance';
const page = Number(queryParams.get('page')) || 0;
const celebId = Number(queryParams.get('celebId')) || null;
const category = queryParams.get('category') || null;
const lowLatitude = Number(queryParams.get('lowLatitude'));
const highLatitude = Number(queryParams.get('highLatitude'));
const lowLongitude = Number(queryParams.get('lowLongitude'));
const highLongitude = Number(queryParams.get('highLongitude'));

const filteredRestaurants = restaurants.filter(({ celebs, category: restaurantCategory }) => {
const hasCelebId = celebId ? celebs.map(({ id }) => id).includes(celebId) : true;
const isMatchCategory = category ? category === restaurantCategory : true;
return hasCelebId && isMatchCategory;
});

const sortedRestaurants = filteredRestaurants
.filter(restaurant => {
return (
restaurant.lat >= lowLatitude &&
restaurant.lat <= highLatitude &&
restaurant.lng >= lowLongitude &&
restaurant.lng <= highLongitude
);
})
.sort((prev, current) => {
if (sort === 'like') return current.likeCount - prev.likeCount;
return prev.distance - current.distance;
});

function moveCelebToFrontById(celebs: Celeb[], targetId: number): Celeb[] {
const targetIndex = celebs.findIndex(celeb => celeb.id === targetId);

if (targetIndex === -1) return celebs;

const newArray = [...celebs];
const [movedCeleb] = newArray.splice(targetIndex, 1);
newArray.unshift(movedCeleb);

return newArray;
}

const content: RestaurantData[] = sortedRestaurants
.slice(page * pageSize, (page + 1) * pageSize)
.map(({ celebs, ...etc }) => {
const sortedCelebs: Celeb[] = moveCelebToFrontById(celebs, celebId);
return { celebs: sortedCelebs, ...etc };
});

const restaurantListData: RestaurantListData = {
content,
currentElementsCount: content.length,
currentPage: page,
pageSize,
totalElementsCount: sortedRestaurants.length,
totalPage: Math.ceil(sortedRestaurants.length / pageSize),
};

return res(ctx.status(200), ctx.json(restaurantListData));
}),

rest.get('/celebs', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(celebs));
}),

rest.get('/oauth/login/:oauthType', (req, res, ctx) => {
const code = req.url.searchParams.get('code') ?? null;

if (code === null) {
return res(ctx.status(401), ctx.json({ message: '인증되지 않은 code입니다.' }));
}

const currentDate = new Date();
const sixHoursInMilliseconds = 6 * 60 * 60 * 1000;
const expirationDate = new Date(currentDate.getTime() + sixHoursInMilliseconds);

return res(ctx.cookie('JSESSION', `${code}`, { expires: expirationDate }), ctx.status(200));
}),

rest.get('/oauth/logout/:oauthType', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

return res(ctx.status(200), ctx.cookie('JSESSION', '', { expires: new Date() }));
}),

rest.delete('/oauth/withdraw/:oauthType', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

// 회원 탈퇴 시에 쿠키 바로 만료
return res(ctx.status(204), ctx.cookie('JSESSION', '', { expires: new Date() }));
}),

rest.get('/members/my', (req, res, ctx) => {
const { JSESSION } = req.cookies;

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

// 쿠키 갱신
return res(ctx.status(200), ctx.json(profile));
}),

rest.post('/restaurants/:restaurantId/like', (req, res, ctx) => {
const { JSESSION } = req.cookies;
const { restaurantId } = req.params;

const restaurant = restaurants.find(restaurant => restaurant.id === Number(restaurantId));
restaurant.isLiked ? (restaurant['isLiked'] = false) : (restaurant['isLiked'] = true);

const responses = [res(ctx.status(429)), res(ctx.status(429)), res(ctx.status(200))];
return responses[getRandomNumber()];

if (JSESSION === undefined) {
return res(ctx.status(401), ctx.json({ message: '만료된 세션입니다.' }));
}

return res(ctx.status(200));
}),
];

export const MainPageErrorHandler = [
rest.post('/restaurants/:restaurantId/like', (req, res, ctx) => {
return res(ctx.status(401));
}),

rest.delete('/oauth/withdraw/:oauthType', (req, res, ctx) => {
return res(ctx.status(401));
}),
];
Loading

0 comments on commit 7ac6f37

Please sign in to comment.