Skip to content

Commit

Permalink
[Feature/FE] 모달 컴포넌트 구현 및 alert, confirm에 적용 (#308)
Browse files Browse the repository at this point in the history
* [FE] 모달 UI 구현 (#305)

feat: 모달 UI 구현 및 스토리 작성

* [FE] 모달로 window.alert 및 window.confirm 대체 (#307)

* feat: modal 렌더링 컨텍스트 구현

* feat: 모달 스타일 수정

* chore: 이벤트 핸들러에서 promise 사용할 수 있도록 lint 설정 변경

* feat: useModal 훅 구현 및 적용
  • Loading branch information
uk960214 authored and Ohzzi committed Aug 2, 2022
1 parent e9a187a commit 6910731
Show file tree
Hide file tree
Showing 21 changed files with 391 additions and 42 deletions.
8 changes: 7 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
"prettier"
],
"rules": {
"react-hooks/exhaustive-deps": 0
"react-hooks/exhaustive-deps": 0,
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
]
}
}
4 changes: 3 additions & 1 deletion frontend/src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import Loading from '@/components/common/Loading/Loading';
import ROUTES from '@/constants/routes';
import { UserDataContext } from '@/contexts/LoginContextProvider';
import useAuth from '@/hooks/useAuth';
import useModal from '@/hooks/useModal';
import { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

function Login() {
const { login } = useAuth();
const { showAlert } = useModal();
const [searchParam] = useSearchParams();
const userData = useContext(UserDataContext);

const navigate = useNavigate();

useEffect(() => {
login(searchParam.get('code')).catch(() => {
alert('로그인에 실패했습니다. 잠시 후 다시 시도해주세요.');
showAlert('로그인에 실패했습니다. 잠시 후 다시 시도해주세요.');
navigate(ROUTES.HOME);
});
}, []);
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/common/HeaderNav/HeaderNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import * as S from '@/components/common/HeaderNav/HeaderNav.style';
import { useEffect, useState } from 'react';
import CategoryNav from '@/components/common/CategoryNav/CategoryNav';
import useAnimation from '@/hooks/useAnimation';
import useModal from '@/hooks/useModal';

function HeaderNav() {
const { logout, isLoggedIn } = useAuth();
const { showAlert, getConfirm } = useModal();

const [categoryOpen, setCategoryOpen] = useState(false);
const [shouldRenderCategory, handleTransitionEnd, triggerAnimation] =
Expand All @@ -21,13 +23,14 @@ function HeaderNav() {
setCategoryOpen((prevState) => !prevState);
};

const handleLogout: React.MouseEventHandler<HTMLButtonElement> = () => {
if (window.confirm('로그아웃 하시겠습니까?')) {
const handleLogout = async () => {
const confirmation = await getConfirm('로그아웃 하시겠습니까?');
if (confirmation) {
try {
logout();
window.alert('로그아웃이 완료되었습니다.');
showAlert('로그아웃이 완료되었습니다.');
} catch {
window.alert('로그아웃에 실패했습니다. 다시 시도해주세요.');
showAlert('로그아웃에 실패했습니다. 다시 시도해주세요.');
}
}
};
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/components/common/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Modal from '@/components/common/Modal/Modal';
import { PropsWithChildren, useState } from 'react';

export default {
component: Modal,
title: 'Components/Modal',
};

type Props = {
showConfirm: boolean;
};

const Template = ({ showConfirm, children }: PropsWithChildren<Props>) => {
const [show, setShow] = useState(false);

const handleClose = () => {
setShow(false);
};
const handleSubmit = () => {
alert('제출됨');
handleClose();
};
return (
<>
<button onClick={() => setShow(true)}>표시하기</button>
{show && (
<Modal
handleClose={handleClose}
handleConfirm={showConfirm && handleSubmit}
>
{children}
</Modal>
)}
</>
);
};

export const Default = () => (
<Template showConfirm={true}>
<Modal.Title>제목</Modal.Title>
<Modal.Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorum eveniet
sint magnam nemo? Laboriosam animi non error veritatis, illo temporibus
dolorum omnis alias repellat aperiam.
</Modal.Body>
</Template>
);

export const NoTitle = () => (
<Template showConfirm={true}>
<Modal.Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorum eveniet
sint magnam nemo? Laboriosam animi non error veritatis, illo temporibus
dolorum omnis alias repellat aperiam. A illum doloremque voluptas modi
eaque iste voluptate ullam corrupti quibusdam id fugiat, maiores
reprehenderit labore ipsam nemo, aliquam cumque facere nostrum libero fuga
unde.
</Modal.Body>
</Template>
);

export const NoConfirm = () => (
<Template showConfirm={false}>
<Modal.Title>제목</Modal.Title>
<Modal.Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorum eveniet
sint magnam nemo? Laboriosam animi non error veritatis, illo temporibus
dolorum omnis alias repellat aperiam. A illum doloremque voluptas modi
eaque iste voluptate ullam corrupti quibusdam id fugiat, maiores
reprehenderit labore ipsam nemo, aliquam cumque facere nostrum libero fuga
unde.
</Modal.Body>
</Template>
);

export const NoTitleAndConfirm = () => (
<Template showConfirm={false}>
<Modal.Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorum eveniet
sint magnam nemo? Laboriosam animi non error veritatis, illo temporibus
dolorum omnis alias repellat aperiam. A illum doloremque voluptas modi
eaque iste voluptate ullam corrupti quibusdam id fugiat, maiores
reprehenderit labore ipsam nemo, aliquam cumque facere nostrum libero fuga
unde.
</Modal.Body>
</Template>
);
82 changes: 82 additions & 0 deletions frontend/src/components/common/Modal/Modal.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import styled from 'styled-components';

export const Container = styled.section<{ scrollOffset: number }>`
position: absolute;
top: ${({ scrollOffset }) => scrollOffset}px;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;

export const Backdrop = styled.div`
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background-color: #00000033;
height: 100%;
`;

export const Content = styled.section`
width: 30rem;
min-height: 10rem;
padding: 1.5rem;
position: relative;
z-index: 15;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.5rem;
border-radius: 0.5rem;
filter: drop-shadow(2px 2px 5px rgba(0, 0, 0, 0.25));
background-color: ${({ theme }) => theme.colors.white};
`;

export const Title = styled.h1`
font-size: 1.5rem;
`;

export const Body = styled.div``;

export const ButtonContainer = styled.div`
display: flex;
justify-content: center;
gap: 2rem;
`;

export const ActionButton = styled.button`
padding: 0.5rem 1rem;
border-radius: 0.3rem;
border: none;
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.25));
&:hover {
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.25));
}
`;

export const ConfirmButton = styled(ActionButton)`
background-color: ${({ theme }) => theme.colors.primary};
`;

export const CloseButton = styled(ActionButton)`
background-color: ${({ theme }) => theme.colors.secondary};
`;
68 changes: 68 additions & 0 deletions frontend/src/components/common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createPortal } from 'react-dom';
import * as S from '@/components/common/Modal/Modal.style';
import { PropsWithChildren, useEffect, useState } from 'react';

type Props = {
handleClose: () => void;
handleConfirm?: () => void;
};

function Modal({
handleClose,
handleConfirm,
children,
}: PropsWithChildren<Props>) {
const [scrollOffset, setScrollOffset] = useState(0);

useEffect(() => {
document.body.style.overflow = 'hidden';
setScrollOffset(window.pageYOffset);

return () => {
document.body.style.overflow = 'auto';
};
}, []);

return createPortal(
<S.Container scrollOffset={scrollOffset}>
<S.Backdrop onClick={handleClose} />
<S.Content>
{children}
<ActionButtons
handleClose={handleClose}
handleConfirm={handleConfirm}
/>
</S.Content>
</S.Container>,
document.querySelector('#root')
);
}

function Title({ children }: PropsWithChildren) {
return <S.Title>{children}</S.Title>;
}

function Body({ children }: PropsWithChildren) {
return <S.Body>{children}</S.Body>;
}

type ActionButtonProps = {
handleClose: () => void;
handleConfirm?: () => void;
};

function ActionButtons({ handleClose, handleConfirm }: ActionButtonProps) {
return (
<S.ButtonContainer>
<S.CloseButton onClick={handleClose}>닫기</S.CloseButton>
{handleConfirm && (
<S.ConfirmButton onClick={handleConfirm}>확인</S.ConfirmButton>
)}
</S.ButtonContainer>
);
}

Modal.Title = Title;
Modal.Body = Body;

export default Modal;
60 changes: 60 additions & 0 deletions frontend/src/contexts/ModalContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Modal from '@/components/common/Modal/Modal';
import { createContext, PropsWithChildren, useState } from 'react';

export const ShowAlertContext = createContext<(message: string) => void>(null);
export const GetConfirmContext =
createContext<(message: string) => Promise<boolean>>(null);

let resolveConfirm: (value: boolean | PromiseLike<boolean>) => void;
function ModalContextProvider({ children }: PropsWithChildren) {
const [message, setMessage] = useState('');
const [alertOpen, setAlertOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);

const showAlert = (message: string) => {
setMessage(message);
setAlertOpen(true);
};
const showConfirm = (message: string) => {
setMessage(message);
setConfirmOpen(true);
};

const handleClose = () => {
setAlertOpen(false);
setConfirmOpen(false);
};

const handleConfirm = () => {
resolveConfirm(true);
setConfirmOpen(false);
};

const getConfirm: (message: string) => Promise<boolean> = (message) => {
showConfirm(message);

return new Promise((resolve) => {
resolveConfirm = resolve;
});
};

return (
<ShowAlertContext.Provider value={showAlert}>
<GetConfirmContext.Provider value={getConfirm}>
{children}
{alertOpen && (
<Modal handleClose={handleClose}>
<Modal.Body>{message}</Modal.Body>
</Modal>
)}
{confirmOpen && (
<Modal handleClose={handleClose} handleConfirm={handleConfirm}>
<Modal.Body>{message}</Modal.Body>
</Modal>
)}
</GetConfirmContext.Provider>
</ShowAlertContext.Provider>
);
}

export default ModalContextProvider;
3 changes: 2 additions & 1 deletion frontend/src/hooks/api/useDelete.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AxiosRequestHeaders } from 'axios';
import useAxios from '@/hooks/api/useAxios';
import handleError from '@/utils/handleError';
import useError from '@/hooks/useError';

type Props = {
url: string;
Expand All @@ -9,6 +9,7 @@ type Props = {

function useDelete({ url, headers }: Props): (id: number) => Promise<void> {
const { axiosInstance } = useAxios();
const handleError = useError();

const deleteData = async (id: number) => {
try {
Expand Down
Loading

0 comments on commit 6910731

Please sign in to comment.