-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from all commits
1e514ee
e96f5b8
a1ff6cf
55b0da1
30dfd7d
b58dedb
b9576ae
e7ed8cf
41ce014
03dbfbb
5d8dc00
e3fb96c
9fb90e9
39452c6
3c0c7f2
9eb0303
a150579
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import ErrorMessage from '.'; | ||
|
||
const meta: Meta<typeof ErrorMessage> = { | ||
component: ErrorMessage, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof ErrorMessage>; | ||
|
||
export const Default: Story = { | ||
render: () => <ErrorMessage errorHandler={() => {}} />, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import IconButton from '../IconButton'; | ||
import SquareButton from '../SquareButton'; | ||
|
||
import * as S from './style'; | ||
|
||
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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
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; | ||
font-weight: bold; | ||
|
||
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 RetryText = styled.p` | ||
display: flex; | ||
justify-content: space-around; | ||
gap: 10px; | ||
|
||
padding: 12px; | ||
|
||
font: var(--text-body); | ||
font-weight: bold; | ||
`; | ||
|
||
export const ButtonWrapper = styled.div` | ||
width: 120px; | ||
height: 50px; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: { | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존 컴포넌트 업데이트를 꾸준히 하시는 모습 한 수 배워갑니다 👍👍👍 |
||
}, | ||
}; | ||
|
||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { | ||
|
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 />, | ||
}; |
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 }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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={() => { | ||
window.location.reload(); | ||
}} | ||
> | ||
새로 고침 | ||
</SquareButton> | ||
</S.ButtonWrapper> | ||
</S.Wrapper> | ||
</Layout> | ||
); | ||
} |
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; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import { AuthContext } from '@hooks/context/auth'; | |
import { useToggle } from '@hooks/useToggle'; | ||
|
||
import Accordion from '@components/common/Accordion'; | ||
import GuestProfile from '@components/common/Dashboard/GuestProfile'; | ||
import UserProfile from '@components/common/Dashboard/UserProfile'; | ||
import IconButton from '@components/common/IconButton'; | ||
import Layout from '@components/common/Layout'; | ||
|
@@ -18,12 +19,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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PrivateRoute가 생기니 필요없는 코드가 사라져서 가독성이 올라가서 좋다고 생각돼요 👍👍👍 |
||
const { loggedInfo } = useContext(AuthContext); | ||
const { userInfo } = loggedInfo; | ||
|
||
return ( | ||
<Layout isSidebarVisible={true}> | ||
|
@@ -39,7 +36,7 @@ export default function MyInfo() { | |
</NarrowTemplateHeader> | ||
</S.HeaderWrapper> | ||
<S.ProfileSection> | ||
<UserProfile userInfo={userInfo} /> | ||
{userInfo ? <UserProfile userInfo={userInfo} /> : <GuestProfile />} | ||
</S.ProfileSection> | ||
<S.UserControlSection> | ||
<Accordion title="닉네임 변경"> | ||
|
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 />, | ||
}; |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} |
There was a problem hiding this comment.
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하신 이유가 궁금합니다!
참고사이트-공홈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 useNavigate hook을 쓰든, Navigate 컴포넌트를 쓰든 똑같은 동작을 구현할 수는 있지만..
'컴포넌트에 대한 접근 권한'의 분기점을 처리하고 싶을 때는.. 컴포넌트를 return 해주고 싶었습니다.
우스도 이 부분에 대해 질문 주셨는데, 글로 정리하면서 정확한 기준이 세워진 느낌이네요 감사합니다 ㅎㅎ👍