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

[NDD-312] Toast 개발 완료 (10h / 8h) #171

Merged
merged 21 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0540e8c
feat: 토스트 상태 관리를 위한 EventManger 추가
milk717 Dec 6, 2023
25e58d2
feat: 토스트 틀 구현 완료
milk717 Dec 6, 2023
47f9fa0
design: 토스트 타입별 아이콘 추가
milk717 Dec 6, 2023
3edd41d
fix: autoClose 옵션 꺼져있을 때 호버 이벤트시 토스트 바로 종료되는 문제 수정
milk717 Dec 6, 2023
167a0cb
feat: 토스트 위치 지정 기능 추가
milk717 Dec 6, 2023
0713add
fix: autoClose 타입 변경
milk717 Dec 6, 2023
cc83052
remove: 토스트 업데이트 관련 로직 제거
milk717 Dec 6, 2023
d6c2f4a
feat: collapseToast을 적용해서 토스트가 자연스럽게 사라지도록 구현
milk717 Dec 6, 2023
cb0330b
refactor: 애니메이션 끝나는 이벤트 훅으로 분리
milk717 Dec 6, 2023
a47941e
feat: 토스트 프로그래스바 추가
milk717 Dec 6, 2023
85e5a08
feat: 마우스 호버시 토스트 타이머 멈춤
milk717 Dec 6, 2023
f5f55f3
refactor: useToastContainer 훅 분리
milk717 Dec 6, 2023
32b7734
rename: EventManger -> eventMangerˆ
milk717 Dec 6, 2023
a85d9cf
rename: EventManger -> eventManger 변경 후 import 수정
milk717 Dec 6, 2023
98ba903
rename: Toast -> ToastItem
milk717 Dec 6, 2023
020f773
refactor: toast.type(text) 형식으로 출력할 수 있게 함수 변경
milk717 Dec 6, 2023
c37e73d
design: 토스트 타입에 따라서 다른 프로그래스바 색상 추가
milk717 Dec 6, 2023
3ada210
chore: 토스트 옵션의 타입을 옵셔널로 변경
milk717 Dec 7, 2023
5a45371
refactor: useAnimationEnd 훅 제거하고 컴포넌트에 직접 리스너 달기
milk717 Dec 7, 2023
cfc0ed6
rename: 핸들러 네이밍 수정
milk717 Dec 7, 2023
9853786
design: 토스트 애니메이션 조정
milk717 Dec 7, 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
2 changes: 2 additions & 0 deletions FE/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import GlobalSVGProvider from '@/GlobalSvgProvider';
import AppRouter from '@/AppRouter';
import ModalProvider from './modalProvider';
import { ToastContainer } from '@foundation/Toast/ToastContainer';

function App() {
const queryClient = new QueryClient();
Expand All @@ -20,6 +21,7 @@ function App() {
<AppRouter queryClient={queryClient} />
<GlobalSVGProvider />
<ModalProvider />
<ToastContainer />
</ThemeProvider>
</RecoilRoot>
</QueryClientProvider>
Expand Down
24 changes: 24 additions & 0 deletions FE/src/GlobalSvgProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,30 @@ const spliteSvgCode = (
strokeLinejoin="round"
/>
</symbol>
<symbol id="check-round" viewBox="0 0 24 24">
<path
d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16.78 9.7L11.11 15.37C10.97 15.51 10.78 15.59 10.58 15.59C10.38 15.59 10.19 15.51 10.05 15.37L7.22 12.54C6.93 12.25 6.93 11.77 7.22 11.48C7.51 11.19 7.99 11.19 8.28 11.48L10.58 13.78L15.72 8.64C16.01 8.35 16.49 8.35 16.78 8.64C17.07 8.93 17.07 9.4 16.78 9.7Z"
fill="#63B561"
/>
</symbol>
<symbol id="info-round" viewBox="0 0 24 24">
<path
d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM11.25 8C11.25 7.59 11.59 7.25 12 7.25C12.41 7.25 12.75 7.59 12.75 8V13C12.75 13.41 12.41 13.75 12 13.75C11.59 13.75 11.25 13.41 11.25 13V8ZM12.92 16.38C12.87 16.51 12.8 16.61 12.71 16.71C12.61 16.8 12.5 16.87 12.38 16.92C12.26 16.97 12.13 17 12 17C11.87 17 11.74 16.97 11.62 16.92C11.5 16.87 11.39 16.8 11.29 16.71C11.2 16.61 11.13 16.51 11.08 16.38C11.03 16.26 11 16.13 11 16C11 15.87 11.03 15.74 11.08 15.62C11.13 15.5 11.2 15.39 11.29 15.29C11.39 15.2 11.5 15.13 11.62 15.08C11.86 14.98 12.14 14.98 12.38 15.08C12.5 15.13 12.61 15.2 12.71 15.29C12.8 15.39 12.87 15.5 12.92 15.62C12.97 15.74 13 15.87 13 16C13 16.13 12.97 16.26 12.92 16.38Z"
fill="#477FEE"
/>
</symbol>
<symbol id="warning" viewBox="0 0 24 24">
<path
d="M21.76 15.92L15.36 4.4C14.5 2.85 13.31 2 12 2C10.69 2 9.49998 2.85 8.63998 4.4L2.23998 15.92C1.42998 17.39 1.33998 18.8 1.98998 19.91C2.63998 21.02 3.91998 21.63 5.59998 21.63H18.4C20.08 21.63 21.36 21.02 22.01 19.91C22.66 18.8 22.57 17.38 21.76 15.92ZM11.25 9C11.25 8.59 11.59 8.25 12 8.25C12.41 8.25 12.75 8.59 12.75 9V14C12.75 14.41 12.41 14.75 12 14.75C11.59 14.75 11.25 14.41 11.25 14V9ZM12.71 17.71C12.66 17.75 12.61 17.79 12.56 17.83C12.5 17.87 12.44 17.9 12.38 17.92C12.32 17.95 12.26 17.97 12.19 17.98C12.13 17.99 12.06 18 12 18C11.94 18 11.87 17.99 11.8 17.98C11.74 17.97 11.68 17.95 11.62 17.92C11.56 17.9 11.5 17.87 11.44 17.83C11.39 17.79 11.34 17.75 11.29 17.71C11.11 17.52 11 17.26 11 17C11 16.74 11.11 16.48 11.29 16.29C11.34 16.25 11.39 16.21 11.44 16.17C11.5 16.13 11.56 16.1 11.62 16.08C11.68 16.05 11.74 16.03 11.8 16.02C11.93 15.99 12.07 15.99 12.19 16.02C12.26 16.03 12.32 16.05 12.38 16.08C12.44 16.1 12.5 16.13 12.56 16.17C12.61 16.21 12.66 16.25 12.71 16.29C12.89 16.48 13 16.74 13 17C13 17.26 12.89 17.52 12.71 17.71Z"
fill="#EAC645"
/>
</symbol>
<symbol id="bomb" viewBox="0 0 512 512">
<path
fill="#E05241"
d="M459.1 52.4L442.6 6.5C440.7 2.6 436.5 0 432.1 0s-8.5 2.6-10.4 6.5L405.2 52.4l-46 16.8c-4.3 1.6-7.3 5.9-7.2 10.4c0 4.5 3 8.7 7.2 10.2l45.7 16.8 16.8 45.8c1.5 4.4 5.8 7.5 10.4 7.5s8.9-3.1 10.4-7.5l16.5-45.8 45.7-16.8c4.2-1.5 7.2-5.7 7.2-10.2c0-4.6-3-8.9-7.2-10.4L459.1 52.4zm-132.4 53c-12.5-12.5-32.8-12.5-45.3 0l-2.9 2.9C256.5 100.3 232.7 96 208 96C93.1 96 0 189.1 0 304S93.1 512 208 512s208-93.1 208-208c0-24.7-4.3-48.5-12.2-70.5l2.9-2.9c12.5-12.5 12.5-32.8 0-45.3l-80-80zM200 192c-57.4 0-104 46.6-104 104v8c0 8.8-7.2 16-16 16s-16-7.2-16-16v-8c0-75.1 60.9-136 136-136h8c8.8 0 16 7.2 16 16s-7.2 16-16 16h-8z"
/>
</symbol>
</svg>
);

Expand Down
80 changes: 80 additions & 0 deletions FE/src/components/foundation/Toast/Toast.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { css, keyframes } from '@emotion/react';
import { theme } from '@styles/theme';

const TOAST_GAP = '0.75rem';
export const ToastProgressBarStyle = {
info: css`
background-color: ${theme.colors.toast.info};
`,
success: css`
background-color: ${theme.colors.toast.success};
`,
warning: css`
background-color: ${theme.colors.toast.warning};
`,
error: css`
background-color: ${theme.colors.toast.error};
`,
default: css`
background-color: ${theme.colors.toast.default};
`,
};

export const ToastTypeIconName = {
info: 'info-round',
success: 'check-round',
warning: 'warning',
error: 'bomb',
default: null,
};

export const ToastPositionStyle = {
topLeft: css`
top: ${TOAST_GAP};
left: ${TOAST_GAP};
`,
topRight: css`
top: ${TOAST_GAP};
right: ${TOAST_GAP};
`,
topCenter: css`
top: ${TOAST_GAP};
left: 50%;
transform: translateX(-50%);
z-index: 10000;
`,
bottomLeft: css`
bottom: ${TOAST_GAP};
left: ${TOAST_GAP};
`,
bottomRight: css`
bottom: ${TOAST_GAP};
right: ${TOAST_GAP};
`,
bottomCenter: css`
bottom: ${TOAST_GAP};
left: 50%;
transform: translateX(-50%);
z-index: 10000;
`,
};

export const ToastFadeOutUpAnimation = keyframes`
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-1.5rem);
}
`;

export const ToastProgressBarAnimation = keyframes`
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
`;
29 changes: 29 additions & 0 deletions FE/src/components/foundation/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { css } from '@emotion/react';
import { ToastPositionStyle } from '@foundation/Toast/Toast.styles';
import useToastContainer from '@foundation/Toast/useToastContainer';
import ToastItem from '@foundation/Toast/ToastItem';

export const ToastContainer = () => {
const { getToastPositionGroupToRender } = useToastContainer();
const positionGroup = getToastPositionGroupToRender();

return Array.from(positionGroup).map(([position, toasts]) => (
<div
key={position}
css={[
css`
position: fixed;
display: flex;
flex-direction: column;
row-gap: 0.5rem;
z-index: 9999;
`,
ToastPositionStyle[position],
]}
>
{toasts.map((toastProps) => (
<ToastItem key={toastProps.toastId} {...toastProps} />
))}
</div>
));
};
117 changes: 117 additions & 0 deletions FE/src/components/foundation/Toast/ToastItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useRef, useState } from 'react';
import { ToastEvent, ToastProps } from '@foundation/Toast/type';
import { css } from '@emotion/react';
import { theme } from '@styles/theme';
import { Box, Icon } from '@foundation/index';
import { collapseToast } from '@foundation/Toast/collapseToast';
import { eventManager } from '@foundation/Toast/eventManger';
import {
ToastFadeOutUpAnimation,
ToastProgressBarAnimation,
ToastProgressBarStyle,
ToastTypeIconName,
} from '@foundation/Toast/Toast.styles';

const ToastItem: React.FC<ToastProps> = ({
toastId,
text,
autoClose = 3000,
closeOnClick = true,
type = 'default',
pauseOnHover = true,
}) => {
const toastRef = useRef<HTMLDivElement>(null);
const [isExiting, setIsExiting] = useState(false);
const [isPaused, setIsPaused] = useState(false);

const handleExitingAnimationEnd = () => {
collapseToast(toastRef.current!, () => {
eventManager.emit(ToastEvent.Delete, toastId);
});
};

const handleProgressAnimationEnd = () => {
autoClose && setIsExiting(true);
};

const handleClick = () => {
closeOnClick && handleProgressAnimationEnd();
};

const handleMouseEnter = () => {
pauseOnHover && autoClose && setIsPaused(true);
};

const handleMouseLeave = () => {
if (pauseOnHover && autoClose) {
setIsPaused(false);
}
};

const IconType = () =>
type === 'default' ? null : (
<Icon id={ToastTypeIconName[type]} width="20" height="20" />
);

return (
<div ref={toastRef}>
<Box
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onAnimationEnd={handleExitingAnimationEnd}
css={[
css`
position: relative;
display: flex;
flex-direction: column;
row-gap: 0.5rem;
min-width: 20rem;
border-radius: 0.5rem;
overflow: hidden;
background-color: ${theme.colors.surface.default};
animation: ${isExiting
? css`
${ToastFadeOutUpAnimation} 0.8s forwards
`
: 'none'};
`,
]}
>
<div
css={css`
display: flex;
align-items: center;
column-gap: 1rem;
padding: 1rem;
`}
>
<IconType />
{text}
</div>
<div
onAnimationEnd={handleProgressAnimationEnd}
css={[
css`
position: absolute;
bottom: 0;
height: 0.3125rem;
width: 100%;
transform-origin: left;
animation: ${autoClose
? css`
${ToastProgressBarAnimation} ${autoClose}ms linear
forwards
`
: 'none'};
animation-play-state: ${isPaused ? 'paused' : 'running'};
`,
ToastProgressBarStyle[type],
]}
/>
</Box>
</div>
);
};

export default ToastItem;
17 changes: 17 additions & 0 deletions FE/src/components/foundation/Toast/collapseToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function collapseToast(
node: HTMLDivElement,
done: () => void,
duration = 1000
) {
const { scrollHeight, style } = node;

requestAnimationFrame(() => {
style.height = scrollHeight + 'px';
style.transition = `all ${duration}ms`;

requestAnimationFrame(() => {
style.height = '0';
setTimeout(done, duration);
});
});
}
55 changes: 55 additions & 0 deletions FE/src/components/foundation/Toast/eventManger.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

[p-5] 사용자 지정의 eventManager를 새로 만드신거군요?!

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { EventManager } from '@foundation/Toast/type';

export const eventManager: EventManager = {
list: new Map(), // 이벤트 리스너 저장
emitQueue: new Map(), // 이벤트 지연을 위한 큐

// 새로운 이벤트 리스너 등록
on(event, callback) {
this.list.has(event)
? this.list.get(event)!.push(callback)
: this.list.set(event, [callback]);

return this;
},

// 콜백이 있는경우 해당하는 리스너 제거, 없는 경우 이벤트에 대한 모든 리스너 제거
off(event, callback) {
if (callback) {
const cb = this.list.get(event)?.filter((cb) => cb !== callback);
cb && this.list.set(event, cb);
return this;
}
this.list.delete(event);

return this;
},

//대기중인 이벤트를 취소시킬 때 필요함
cancelEmit(event) {
const timers = this.emitQueue.get(event);
if (timers) {
timers.forEach(clearTimeout);
this.emitQueue.delete(event);
}

return this;
},

// 이벤트 발생시키기
// 타입문제는 일단 보류중! (라이브러리 내부도 해결 안되어있음)
Copy link
Collaborator

Choose a reason for hiding this comment

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

[p-5] 타입의 세계란 너무 어려운것입니다...

emit(event, ...args: never[]) {
this.list.has(event) &&
this.list.get(event)!.forEach((callback) => {
const timer = setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return callback(...args);
}, 0);

this.emitQueue.has(event)
? this.emitQueue.get(event)!.push(timer)
: this.emitQueue.set(event, [timer]);
});
},
};
Comment on lines +41 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

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

[p-5] 블로그글을 읽으면서 보고 있는데 "콜백함수가 동기함수가 아닐 경우를 대비" 이게 어떤 경우인지 모르겠습니다. 비동기 함수가 콜백으로 들어왔을때 해당 주기를 밀어내지 않으면 어떤 사이드 이펙트가 있는건가요?

30 changes: 30 additions & 0 deletions FE/src/components/foundation/Toast/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ToastEvent, ToastProps, ToastType } from '@foundation/Toast/type';
import { eventManager } from '@foundation/Toast/eventManger';

type ToastFunctionProps = Omit<ToastProps, 'toastId' | 'type'>;
type ToastOptions = Omit<ToastProps, 'toastId' | 'type' | 'text'>;
const generateUniqueId = () => {
return Date.now().toString(36) + Math.random().toString(36).substring(4);
};

const emitToast = (type: ToastType, toastProps: ToastFunctionProps) => {
const id = generateUniqueId();
Copy link
Collaborator

Choose a reason for hiding this comment

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

[p-5] useId라는 hook은 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사용하고싶었지만... 이곳이 훅이 아니라서 안됩니다ㅜㅜ

eventManager.emit(ToastEvent.Add, {
...toastProps,
toastId: id,
type,
});
};

export const toast = {
default: (text: string, toastOptions?: ToastOptions) =>
emitToast('default', { text: text, ...toastOptions }),
info: (text: string, toastOptions?: ToastOptions) =>
emitToast('info', { text: text, ...toastOptions }),
success: (text: string, toastOptions?: ToastOptions) =>
emitToast('success', { text: text, ...toastOptions }),
warning: (text: string, toastOptions?: ToastOptions) =>
emitToast('warning', { text: text, ...toastOptions }),
error: (text: string, toastOptions?: ToastOptions) =>
emitToast('error', { text: text, ...toastOptions }),
};
Loading
Loading