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

사이드바에 토스트가 가려지던 문제 해결 (feat.createPortal) #841

Merged
merged 6 commits into from
Nov 20, 2023
1 change: 1 addition & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@
</head>
<body>
<div id="root"></div>
<div id="toast-content"></div>
</body>
</html>
7 changes: 5 additions & 2 deletions frontend/src/components/ToastContainer/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { theme } from '@styles/theme';

// 컨테이너를 가로중앙에 위치시키기 위해 left = width * 1/2로 설정
export const Container = styled.div`
width: 80%;
width: 100vw;

position: fixed;
bottom: 20vh;
left: 10%;
left: auto;
right: auto;

padding: 0 10%;

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

Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/common/Drawer/DrawerToastWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { HTMLAttributes } from 'react';

import { ToastContentId } from '@type/toast';

import * as S from './style';

interface DrawerToastWrapperProps extends HTMLAttributes<HTMLDivElement> {
id: ToastContentId;
placement: 'left' | 'right';
}

export default function DrawerToastWrapper({ placement, ...rest }: DrawerToastWrapperProps) {
return <S.ToastWrapper {...rest} $placement={placement} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { styled } from 'styled-components';

export const ToastWrapper = styled.div<{ $placement: 'left' | 'right' }>`
position: absolute;
width: 100vw;
left: ${({ $placement }) => ($placement === 'left' ? '0' : 'auto')};
right: ${({ $placement }) => ($placement === 'right' ? '0' : 'auto')};
`;
8 changes: 1 addition & 7 deletions frontend/src/components/common/Drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import React, {
ForwardedRef,
KeyboardEvent,
MouseEvent,
PropsWithChildren,
forwardRef,
} from 'react';
import { ForwardedRef, KeyboardEvent, MouseEvent, PropsWithChildren, forwardRef } from 'react';

import * as S from './style';

Expand Down
31 changes: 21 additions & 10 deletions frontend/src/hooks/context/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { PropsWithChildren, createContext, useEffect, useRef, useState } from 'react';
import { PropsWithChildren, createContext, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { ToastContentId } from '@type/toast';

import ToastContainer from '@components/ToastContainer';

Expand All @@ -11,17 +14,32 @@ export interface ToastInfo {

interface ToastContextProps {
addMessage: (message: string) => void;
setElementId: (id: ToastContentId) => void;
}

export const ToastContext = createContext<ToastContextProps>({
addMessage: (message: string) => {},
setElementId: () => {},
});

export default function ToastProvider({ children }: PropsWithChildren) {
const [toastList, setToastList] = useState<ToastInfo[]>([]);
const [toastElementId, setToastElementId] = useState<ToastContentId>('toast-content');
const toastContentEl = document.getElementById(toastElementId);

Copy link
Collaborator

Choose a reason for hiding this comment

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

다시 읽으며 든 생각인데, document.getElementById로 dom을 직접 제어하는 것은 지양된다고 알고 있습니다.
real dom 과 가상돔이 달라지면서 돔 요소를 신뢰할 수 없어진다고 알고 있습니다.
위험부담을 감안하여 진행해야 할까요? 고민이 듭니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이 부분은 잘 모르겠네요.

다만 공식문서에서 있는 방법을 참고하여서 진행했기 때문에 괜찮지 않을까라는 생각이 듭니다

const timeId = useRef<number | null>(null);

const addMessage = (message: string) => {
if (toastList.find(toast => toast.text === message)) return;

const id = Date.now();
setToastList(toastList => [...toastList, { id, text: message }]);
};

const setElementId = useCallback((id: ToastContentId) => {
setToastElementId(id);
Copy link
Member

Choose a reason for hiding this comment

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

리렌더링 방지👍👍

}, []);

useEffect(() => {
if (timeId.current) window.clearTimeout(timeId.current);

Expand All @@ -34,16 +52,9 @@ export default function ToastProvider({ children }: PropsWithChildren) {
}
}, [toastList]);

const addMessage = (message: string) => {
if (toastList.find(toast => toast.text === message)) return;

const id = Date.now();
setToastList(toastList => [...toastList, { id, text: message }]);
};

return (
<ToastContext.Provider value={{ addMessage }}>
<ToastContainer toastList={toastList} />
<ToastContext.Provider value={{ addMessage, setElementId }}>
{toastContentEl && createPortal(<ToastContainer toastList={toastList} />, toastContentEl)}
{children}
</ToastContext.Provider>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/alarm/useReadAlarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useReadAlarm = (type: AlarmType) => {
const alarmQueryKey = type === 'CONTENT' ? QUERY_KEY.ALARM_CONTENT : QUERY_KEY.ALARM_REPORT;

const { mutate } = useMutation({
mutationFn: async (alarmId: number) => await readAlarm(alarmId, type),
mutationFn: (alarmId: number) => readAlarm(alarmId, type),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [alarmQueryKey] });
},
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/hooks/query/report/useReportAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export const useReportAction = () => {
const { addMessage } = useContext(ToastContext);

const { mutate, isLoading, isSuccess, isError, error } = useMutation({
mutationFn: async (reportActionData: ReportActionRequest) =>
await reportAction(reportActionData),
mutationFn: (reportActionData: ReportActionRequest) => reportAction(reportActionData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.REPORT] });
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/report/useReportContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const useReportContent = () => {
const { addMessage } = useContext(ToastContext);

const { mutate, isLoading } = useMutation({
mutationFn: async (reportData: ReportRequest) => await reportContent(reportData),
mutationFn: (reportData: ReportRequest) => reportContent(reportData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.REPORT] });
addMessage('신고를 완료하였습니다.');
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/hooks/query/useReportApproveResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export const useReportApproveResult = (reportId: number) => {
suspense: true,

retry: (failCount, error) => {
// const fetchError = error as Error;
// const status = JSON.parse(fetchError.message).status;
// if (status === 404) {
// return false;
// }
const fetchError = error as Error;
const status = JSON.parse(fetchError.message).status;
if (status === 404) {
return false;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍👍

return failCount <= 3;
},
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/user/useReadLatestAlarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const useReadLatestAlarm = () => {
const queryClient = useQueryClient();

const { mutate } = useMutation({
mutationFn: async () => await readLatestAlarm(),
mutationFn: readLatestAlarm,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_INFO] });
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/user/useUpdateUserInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useUpdateUserInfo = () => {

const LOGGED_IN = true;
const { mutate, isLoading, isSuccess, isError, error } = useMutation({
mutationFn: async (userInfo: UpdateUserInfoRequest) => await updateUserInfo(userInfo),
mutationFn: (userInfo: UpdateUserInfoRequest) => updateUserInfo(userInfo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_INFO, LOGGED_IN] });

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/user/useWithdrawalMembership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useWithdrawalMembership = () => {

const LOGGED_IN = true;
const { mutate, isLoading, isSuccess, isError, error } = useMutation({
mutationFn: async () => await withdrawalMembership(),
mutationFn: withdrawalMembership,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.USER_INFO, LOGGED_IN] });
addMessage('회원 탈퇴를 완료했습니다.');
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const useDrawer = (placement: 'left' | 'right') => {

drawerRef.current.style.transform =
placement === 'left' ? 'translateX(-100%)' : 'translateX(100%)';
}, []);
}, [placement]);

return { drawerRef, openDrawer, closeDrawer };
};
28 changes: 24 additions & 4 deletions frontend/src/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import AddButton from '@components/common/AddButton';
import AppInstallPrompt from '@components/common/AppInstallPrompt';
import Dashboard from '@components/common/Dashboard';
import Drawer from '@components/common/Drawer';
import DrawerToastWrapper from '@components/common/Drawer/DrawerToastWrapper';
import Layout from '@components/common/Layout';
import NarrowMainHeader from '@components/common/NarrowMainHeader';
import Skeleton from '@components/common/Skeleton';
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function HomePage() {
closeDrawer: closeAlarmDrawer,
} = useDrawer('right');

const { setElementId } = useContext(ToastContext);
const { isBannerOpen, closeBanner } = useBannerToggle();
const { addMessage } = useContext(ToastContext);
const loggedInfo = useContext(AuthContext).loggedInfo;
Expand All @@ -55,15 +57,31 @@ export default function HomePage() {
if (!loggedInfo.isLoggedIn) return addMessage('알림은 로그인 후 이용할 수 있습니다.');

openAlarmDrawer();
setElementId('drawer-alarm-toast-content');
mutate();
};

const handleAlarmDrawerClose = () => {
closeAlarmDrawer();
setElementId('toast-content');
};

const handleCategoryDrawerOpen = () => {
openCategoryDrawer();
setElementId('drawer-category-toast-content');
};

const handleCategoryDrawerClose = () => {
closeCategoryDrawer();
setElementId('toast-content');
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기까지 오기 위한 빌드업이었군요!
읽으면서 헷갈리는 부분도 있었는데, 이 부분을 읽으니 로직이 확 이해가 갔습니다 👍👍
설계하는 능력이 뛰어나시네요


return (
<Layout isSidebarVisible={true} isMobileDefaultHeaderVisible={false}>
<S.Container>
<S.HeaderWrapper>
<NarrowMainHeader
handleCategoryOpenClick={openCategoryDrawer}
handleCategoryOpenClick={handleCategoryDrawerOpen}
handleAlarmOpenClick={handleToolTipOpen}
isAlarmActive={isAlarmActive ?? false}
/>
Expand All @@ -79,21 +97,23 @@ export default function HomePage() {
)}
<S.DrawerWrapper>
<Drawer
handleDrawerClose={closeCategoryDrawer}
handleDrawerClose={handleCategoryDrawerClose}
placement="left"
width="225px"
ref={categoryDrawerRdf}
>
<DrawerToastWrapper placement="left" id="drawer-category-toast-content" />
<Dashboard />
</Drawer>
<Drawer
handleDrawerClose={closeAlarmDrawer}
handleDrawerClose={handleAlarmDrawerClose}
placement="right"
width="310px"
ref={alarmDrawerRef}
>
<DrawerToastWrapper id="drawer-alarm-toast-content" placement="right" />
{loggedInfo.isLoggedIn && (
<AlarmContainer closeToolTip={closeAlarmDrawer} style={alarmDrawerStyle} />
<AlarmContainer closeToolTip={handleAlarmDrawerClose} style={alarmDrawerStyle} />
)}
</Drawer>
</S.DrawerWrapper>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/types/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ToastContentId =
| 'toast-content'
| 'drawer-category-toast-content'
| 'drawer-alarm-toast-content';

export type DrawerToastContentId = Exclude<ToastContentId, 'toast-content'>;
Loading