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

인증/인가에 따른 라우팅 구현, API 통신 실패 및 존재하지 않는 페이지(Not Found)에 대한 Fallback UI 구현 #343

Merged
merged 17 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1e514ee
chore: (#183) 불필요한 모듈 삭제
inyeong-kang Aug 10, 2023
e96f5b8
refactor: (#183) 의존성 배열 추가
inyeong-kang Aug 10, 2023
a1ff6cf
feat: (#253) NotFound 컴포넌트 구현
inyeong-kang Aug 10, 2023
55b0da1
feat: (#183) PrivateRoute 구현 및 필요한 페이지에 적용
inyeong-kang Aug 10, 2023
30dfd7d
feat: (#253) 헤더 및 로고 추가
inyeong-kang Aug 10, 2023
b58dedb
feat: (#325) Error 컴포넌트 구현 및 소셜 로그인 요청 실패 케이스에 적용
inyeong-kang Aug 10, 2023
b9576ae
refactor: (#325) 다시 시도 라는 문구로 수정
inyeong-kang Aug 10, 2023
e7ed8cf
chore: (#325) 불필요한 코드 삭제
inyeong-kang Aug 10, 2023
41ce014
feat: (#325) Error 컴포넌트 구현 및 get 요청 실패 케이스에 적용
inyeong-kang Aug 11, 2023
03dbfbb
feat: (#325) IconButton에 retry 아이콘 추가 및 Error 컴포넌트 디자인 수정
inyeong-kang Aug 11, 2023
5d8dc00
refactor: (#325) Redirection 페이지에 로그인 요청에 대한 로딩 및 에러 컴포넌트 적용, errorEl…
inyeong-kang Aug 11, 2023
e3fb96c
refactor: (#183) 전역 상태 대신 cookie 유무로 navigate하도록 수정
inyeong-kang Aug 11, 2023
9fb90e9
refactor: (#183) navigate 대신 Navigate로 수정, 권한 관련 props 추가
inyeong-kang Aug 11, 2023
39452c6
feat: (#183) 작성자인 경우에만 글 수정, 투표 통계 페이지 접근하도록 라우팅 설정
inyeong-kang Aug 11, 2023
3c0c7f2
refactor: (#183) Navigate 불필요한 속성 제거, 페이지 접근 불가능한 경우 alert 구현
inyeong-kang Aug 12, 2023
9eb0303
refactor: (#183) props에 할당한 값에 대한 타입 단언 대신, 조건부로 대체 컴포넌트 렌더링하도록 수정
inyeong-kang Aug 12, 2023
a150579
refactor: (#183) Error 컴포넌트 이름 ErrorMessage로 수정, Error 페이지 및 ErrorMes…
inyeong-kang Aug 12, 2023
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
3 changes: 3 additions & 0 deletions frontend/src/assets/retry.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion frontend/src/components/PostForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UseMutateFunction } from '@tanstack/react-query';

import React, { HTMLAttributes, useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Navigate, useNavigate } from 'react-router-dom';

import { PostInfo } from '@type/post';

Expand All @@ -20,9 +20,11 @@ import SquareButton from '@components/common/SquareButton';
import TimePickerOptionList from '@components/common/TimePickerOptionList';
import WritingVoteOptionList from '@components/optionList/WritingVoteOptionList';

import { PATH } from '@constants/path';
import { POST_DESCRIPTION_MAX_LENGTH, POST_TITLE_MAX_LENGTH } from '@constants/post';

import { changeCategoryToOption } from '@utils/post/changeCategoryToOption';
import { checkWriter } from '@utils/post/checkWriter';
import { addTimeToDate, formatTimeWithOption } from '@utils/post/formatTime';
import { getDeadlineTime } from '@utils/post/getDeadlineTime';

Expand All @@ -43,13 +45,15 @@ const CATEGORY_COUNT_LIMIT = 3;

export default function PostForm({ data, mutate, isError, error }: PostFormProps) {
const {
postId,
title,
content,
category: categoryIds,
createTime,
deadline,
voteInfo,
imageUrl,
writer,
} = data ?? {};

const navigate = useNavigate();
Expand Down Expand Up @@ -142,6 +146,8 @@ export default function PostForm({ data, mutate, isError, error }: PostFormProps
}
};

if (postId && writer && !checkWriter(writer.id)) return <Navigate to={PATH.HOME} />;
Copy link
Collaborator

@chsua chsua Aug 12, 2023

Choose a reason for hiding this comment

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

찾아보니 navigate / useNavigate / useNavigation / link 등 여러가지 페이지 이동 수단이 있는 것 것은데 구분점을 잘 모르겠네요
한 블로그에서 useNavigate 를 사용하기 어려울 때 navigate를 사용하고, 가능하면 useNavigate를 사용하라는 말이 있다고 하네요.
혹시 제로가 생각하시는 각 사용처나 구분점 같은게 있으신가요?
useNavigate가 import되어있는데 navigate르 import하신 이유가 궁금합니다!

참고사이트-공홈

Copy link
Member Author

@inyeong-kang inyeong-kang Aug 12, 2023

Choose a reason for hiding this comment

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

사실 useNavigate hook을 쓰든, Navigate 컴포넌트를 쓰든 똑같은 동작을 구현할 수는 있지만..

'컴포넌트에 대한 접근 권한'의 분기점을 처리하고 싶을 때는.. 컴포넌트를 return 해주고 싶었습니다.

  1. 권한이 없다 -> Navigate (JSX 컴포넌트) 를 return하여 redirect 시키기
  2. 권한이 있다 -> 의도했던 컴포넌트를 return

우스도 이 부분에 대해 질문 주셨는데, 글로 정리하면서 정확한 기준이 세워진 느낌이네요 감사합니다 ㅎㅎ👍


return (
<>
<S.HeaderWrapper>
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/common/Error/Error.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';

import Error from '.';

const meta: Meta<typeof Error> = {
component: Error,
};

export default meta;
type Story = StoryObj<typeof Error>;

export const Default: Story = {
render: () => <Error />,
};
19 changes: 19 additions & 0 deletions frontend/src/components/common/Error/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import IconButton from '../IconButton';

import * as S from './style';

export default function Error() {
return (
<S.Wrapper>
<S.Title>⚠ 잠시 후 다시 시도해주세요.</S.Title>
<S.Description>요청 사항을 처리하는데 실패했습니다.</S.Description>
<S.Direction>
<S.Text>
<IconButton category="retry" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

이벤트는 추후에 사용하는 곳에서 붙이는 건가요?

Copy link
Member Author

@inyeong-kang inyeong-kang Aug 12, 2023

Choose a reason for hiding this comment

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

넵 맞습니다!
props로 errorHandler 받아서 onClick에 할당하는 방식으로 수정했습니다.

export default function ErrorMessage({ errorHandler }: { errorHandler: () => void }) {
  return (
    <S.Wrapper>
      <S.Title> 잠시  다시 시도해주세요.</S.Title>
      <S.Description>요청하신 데이터를 불러오는데 실패했습니다.</S.Description>
      <S.Direction>
        <SquareButton onClick={errorHandler} aria-label="다시 시도" theme="blank"> // 수정✅
          <S.RetryText>
            <IconButton category="retry" />
            다시 시도
          </S.RetryText>
        </SquareButton>
      </S.Direction>
    </S.Wrapper>
  );
}

(해당 컴포넌트 이름은 Error 에서 ErrorMessage로 수정하였습니다~)

다시시도
</S.Text>
<S.RetryText>오류가 지속되는 경우 votogether@gmail.com 로 문의해주세요.</S.RetryText>
</S.Direction>
</S.Wrapper>
);
}
71 changes: 71 additions & 0 deletions frontend/src/components/common/Error/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;

position: relative;
`;

export const HeaderWrapper = styled.div`
width: 100%;

position: fixed;

z-index: ${theme.zIndex.header};

@media (min-width: ${theme.breakpoint.md}) {
display: none;
}
`;

export const Title = styled.h1`
width: 90%;
margin-top: 60px;

font-size: 20px;
text-align: center;
`;

export const Description = styled.p`
width: 90%;
margin: 20px 0;

font: var(--text-body);
text-align: center;
`;

export const Direction = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
`;

export const Text = styled.p`
display: flex;
justify-content: center;
align-items: center;
gap: 10px;

font: var(--text-body);
text-decoration: underline;

cursor: pointer;
`;

export const RetryText = styled.p`
color: gray;
font: var(--text-caption);
`;

export const ButtonWrapper = styled.div`
width: 120px;
height: 50px;
`;
7 changes: 6 additions & 1 deletion frontend/src/components/common/IconButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { ButtonHTMLAttributes } from 'react';

import backIcon from '@assets/back.svg';
import categoryIcon from '@assets/category.svg';
import retryIcon from '@assets/retry.svg';
import searchIcon from '@assets/search_white.svg';

import * as S from './style';

type IconCategory = 'category' | 'back' | 'search';
type IconCategory = 'category' | 'back' | 'search' | 'retry';

const ICON_CATEGORY: Record<IconCategory, { name: string; url: string }> = {
category: {
Expand All @@ -21,6 +22,10 @@ const ICON_CATEGORY: Record<IconCategory, { name: string; url: string }> = {
name: '검색',
url: searchIcon,
},
retry: {
name: '다시시도',
url: retryIcon,
Comment on lines +25 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

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

기존 컴포넌트 업데이트를 꾸준히 하시는 모습 한 수 배워갑니다 👍👍👍

},
};

interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/post/PostList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import Skeleton from '@components/common/Skeleton';
import { SORTING_OPTION, STATUS_OPTION } from '@components/post/PostListPage/constants';
import type { PostSorting, PostStatus } from '@components/post/PostListPage/types';

import { SORTING, STATUS } from '@constants/post';

import EmptyPostList from '../EmptyPostList';

import * as S from './style';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/context/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (userInfo && loggedInfo.isLoggedIn) {
setLoggedInfo(origin => ({ ...origin, userInfo }));
}
}, [userInfo]);
}, [loggedInfo.isLoggedIn, userInfo]);

useEffect(() => {
const accessToken = getCookieToken().accessToken;
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/pages/Error/Error.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';

import Error from '.';

const meta: Meta<typeof Error> = {
component: Error,
};

export default meta;
type Story = StoryObj<typeof Error>;

export const Default: Story = {
render: () => <Error />,
};
36 changes: 36 additions & 0 deletions frontend/src/pages/Error/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useNavigate } from 'react-router-dom';

import Layout from '@components/common/Layout';
import SquareButton from '@components/common/SquareButton';

import * as S from './style';

export default function Error({ message }: { message?: string }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error 컴포넌트와 이름이 겹친다면 이름으로 ErrorPage는 어떠세요?

const navigate = useNavigate();

return (
<Layout isSidebarVisible={false}>
<S.Wrapper>
<S.Description>{message ? message : '요청 중 오류가 발생했습니다.'}</S.Description>
<S.ButtonWrapper>
<SquareButton
theme="fill"
onClick={() => {
navigate('/');
}}
>
홈으로 가기
</SquareButton>
<SquareButton
theme="gray"
onClick={() => {
navigate(-1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 일단 임의의 이벤트를 넣으신건가용? 아님 진짜 네비가 들어가나요?

Copy link
Member Author

@inyeong-kang inyeong-kang Aug 12, 2023

Choose a reason for hiding this comment

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

임의의 이벤트를 넣었습니다.. 어떤 동작이 들어가야할지 고민이 되어서..?
일단은 '새로 고침' 버튼으로 대체했어요!

}}
>
다시 시도
</SquareButton>
</S.ButtonWrapper>
</S.Wrapper>
</Layout>
);
}
38 changes: 38 additions & 0 deletions frontend/src/pages/Error/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 40px;

position: relative;
`;

export const HeaderWrapper = styled.div`
width: 100%;

position: fixed;

z-index: ${theme.zIndex.header};
`;

export const Description = styled.p`
width: 90%;
margin-top: 60px;

font: var(--text-title);
text-align: center;
`;

export const ButtonWrapper = styled.div`
display: flex;
justify-content: space-between;
gap: 20px;

width: 280px;
height: 50px;
`;
12 changes: 5 additions & 7 deletions frontend/src/pages/MyInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';

import { User } from '@type/user';

import { AuthContext } from '@hooks/context/auth';
import { useToggle } from '@hooks/useToggle';

Expand All @@ -18,12 +20,8 @@ export default function MyInfo() {
const navigate = useNavigate();
const { isOpen, openComponent, closeComponent } = useToggle();

const { userInfo } = useContext(AuthContext).loggedInfo;

if (!userInfo) {
navigate('/');
return <></>;
}
Comment on lines -21 to -26
Copy link
Collaborator

Choose a reason for hiding this comment

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

PrivateRoute가 생기니 필요없는 코드가 사라져서 가독성이 올라가서 좋다고 생각돼요 👍👍👍

const { loggedInfo } = useContext(AuthContext);
const { userInfo } = loggedInfo;

return (
<Layout isSidebarVisible={true}>
Expand All @@ -39,7 +37,7 @@ export default function MyInfo() {
</NarrowTemplateHeader>
</S.HeaderWrapper>
<S.ProfileSection>
<UserProfile userInfo={userInfo} />
<UserProfile userInfo={userInfo ?? ({} as User)} />
Copy link
Collaborator

Choose a reason for hiding this comment

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

좋은 방법 배워갑니다 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 작성되면 혹시나 실제로 없는데 실행되는 경우

    <PS.ProfileContainer>
      <S.NickName>{undefined}</S.NickName>
      <S.UserInfoContainer>
        <S.TextCardLink to={PATH.USER_POST}>
          <S.TextCardTitle>작성글</S.TextCardTitle>
          <S.TextCardContent>{undefined}</S.TextCardContent>
        </S.TextCardLink>
        <S.TextCardLink to={PATH.USER_VOTE}>
          <S.TextCardTitle>투표수</S.TextCardTitle>
          <S.TextCardContent>{undefined}</S.TextCardContent>
        </S.TextCardLink>
      </S.UserInfoContainer>
    </PS.ProfileContainer>
  );

이렇게 나올 거 같은데 아님 아예 이런 경우를 대비해서 상수로 오류 대체 정보를 만드는 건 어떻게 생각하세요?
제일 좋은 것은 오류가 일어나지 않는거지만 혹시 일어난다면 undefined보단 이해가능한 문자열을 보여주는게 더 좋을 것 같아요!

{
nickname: 오류가 발생했습니다.
 postCount: 0
 voteCount: 0
}

이러면 오류가 발생했습니다,라는 닉네임과 헷갈리려나요

Copy link
Member Author

Choose a reason for hiding this comment

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

오 제안 감사합니다~~!

Copy link
Member Author

Choose a reason for hiding this comment

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

저 닉네임은 사용 못하게 하는 방법도 있을거 같긴 한데.. 좀더 고민해봐야겠네요🤔🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

{
nickname: 오류가 발생했습니다.
 postCount: -1
 voteCount: -1
}

단언 대신 3가지 모두 유효하지 않는 값들의 User 객체로 수정했습니다~

Copy link
Member Author

@inyeong-kang inyeong-kang Aug 12, 2023

Choose a reason for hiding this comment

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

다시 생각해보니까 Dashboard 처럼 분기를 하는 것이 낫겠다는 생각이 듭니다..

// Dashboard 코드 중..
export default function Dashboard({
  userInfo,
  categoryList,
  selectedCategory = '전체',
  handleLogoutClick,
}: DashboardProps) {
  const favoriteCategory = categoryList.filter(category => category.isFavorite === true);
  const allCategory = categoryList.filter(category => category.isFavorite === false);

  return (
    <S.Container>
      {userInfo ? <UserProfile userInfo={userInfo} /> : <GuestProfile />} // <--- 해당 코드✅
      <S.SelectCategoryWrapper>
// 수정한 코드
        <S.ProfileSection>
          {userInfo ? <UserProfile userInfo={userInfo} /> : <GuestProfile />}
        </S.ProfileSection>

</S.ProfileSection>
<S.UserControlSection>
<Accordion title="닉네임 변경">
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/pages/NotFound/NotFound.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';

import NotFound from '.';

const meta: Meta<typeof NotFound> = {
component: NotFound,
};

export default meta;
type Story = StoryObj<typeof NotFound>;

export const Default: Story = {
render: () => <NotFound />,
};
42 changes: 42 additions & 0 deletions frontend/src/pages/NotFound/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useNavigate } from 'react-router-dom';

import IconButton from '@components/common/IconButton';
import Layout from '@components/common/Layout';
import LogoButton from '@components/common/LogoButton';
import NarrowTemplateHeader from '@components/common/NarrowTemplateHeader';
import SquareButton from '@components/common/SquareButton';

import * as S from './style';

export default function NotFound() {
const navigate = useNavigate();
return (
<Layout isSidebarVisible={false}>
<S.Wrapper>
<S.HeaderWrapper>
<NarrowTemplateHeader>
<IconButton
category="back"
onClick={() => {
navigate(-1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게도 사용가능하네요? -3은 3번 뒤로가기라고 하네요 🤣

}}
/>
</NarrowTemplateHeader>
</S.HeaderWrapper>
<S.Title>404</S.Title>
<LogoButton content="icon" style={{ width: '150px', height: '150px' }} />
<S.Description>요청하신 페이지를 찾을 수 없어요.</S.Description>
<S.ButtonWrapper>
<SquareButton
theme="fill"
onClick={() => {
navigate('/');
}}
>
홈으로 가기
</SquareButton>
</S.ButtonWrapper>
</S.Wrapper>
</Layout>
);
}
Loading