Skip to content

Commit 3f86359

Browse files
authored
Merge pull request #107 from Ureca-Mini-Project-Team4/feature/toast
FEAT: Toast 컴포넌트 구현
2 parents 6c8f65c + 6ce4c8a commit 3f86359

File tree

8 files changed

+144
-44
lines changed

8 files changed

+144
-44
lines changed

react-app/src/components/Layout/Layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Outlet } from 'react-router-dom';
2+
import Toast from '../Toast/Toast';
23

34
const Layout = () => {
45
return (
56
<section className="flex flex-col min-h-screen">
7+
<Toast />
68
<Outlet />
79
</section>
810
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useSelector, useDispatch } from 'react-redux';
2+
import { RootState } from '@/store';
3+
import { hide } from '@/store/slices/toastSlice';
4+
5+
export default function Toast() {
6+
const toast = useSelector((state: RootState) => state.toast);
7+
const dispatch = useDispatch();
8+
9+
if (!toast.isShow) return null;
10+
11+
const iconMap = {
12+
success: '/assets/icons/success.svg',
13+
warning: '/assets/icons/warning.svg',
14+
information: '/assets/icons/info.svg',
15+
};
16+
17+
const bgMap = {
18+
success: 'bg-[var(--color-success)]',
19+
warning: 'bg-[var(--color-warning)]',
20+
information: 'bg-[var(--color-info)]',
21+
};
22+
23+
return (
24+
<div className="fixed top-20 sm:top-24 left-1/2 transform -translate-x-1/2 z-50 w-full max-w-xs sm:max-w-sm md:max-w-md">
25+
<div
26+
className={`flex items-center gap-3 shadow-md sm:shadow-xl px-5 py-3 rounded-lg sm:rounded-2xl text-white ${bgMap[toast.type]}`}
27+
onClick={() => dispatch(hide())}
28+
>
29+
<img src={iconMap[toast.type]} alt="icon" className="w-3 h-3 sm:w-4 sm:h-4" />
30+
<span className="text-sm sm:text-base font-medium">{toast.message}</span>
31+
</div>
32+
</div>
33+
);
34+
}

react-app/src/hook/useToast.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useDispatch } from 'react-redux';
2+
import { show, hide, ToastType } from '@/store/slices/toastSlice';
3+
4+
/**
5+
* 사용법
6+
* - const { showToast } = useToast();
7+
* - showToast('성공적으로 완료되었습니다!', 'success')
8+
*/
9+
export function useToast() {
10+
const dispatch = useDispatch();
11+
12+
const showToast = (message: string, type: ToastType = 'information', duration: number = 4000) => {
13+
dispatch(show({ message, type }));
14+
15+
setTimeout(() => {
16+
dispatch(hide());
17+
}, duration);
18+
};
19+
20+
return { showToast };
21+
}

react-app/src/pages/Home.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
const Home = () => {
2-
return <div></div>;
3-
};
1+
import { useToast } from '@/hook/useToast';
42

5-
export default Home;
3+
export default function Home() {
4+
const { showToast } = useToast();
5+
6+
const handleButtonClick = () => {
7+
showToast('버튼이 클릭되었습니다', 'success');
8+
};
9+
10+
return (
11+
<div>
12+
<h1>Hello</h1>
13+
<button onClick={handleButtonClick}>토스트 표시</button>
14+
</div>
15+
);
16+
}

react-app/src/routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Vote from './pages/Vote';
66

77
const router = createBrowserRouter([
88
{
9-
path: '/',
9+
// path: '/',
1010
element: <Layout />,
1111
children: [
1212
{

react-app/src/store/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,27 @@ import storage from 'redux-persist/lib/storage';
1313
import authReducer from './slices/authSlice';
1414
import pageReducer from './slices/pageSlice';
1515
import timerReducer from './slices/timerSlice';
16+
import toastReducer from './slices/toastSlice';
1617

17-
// auth 슬라이스만 영구 저장되도록 설정
18+
// auth 슬라이스만 영구 저장되도록 설정
1819
const persistConfig = {
1920
key: 'root',
2021
storage,
2122
whitelist: ['auth'],
2223
};
2324

24-
// 모든 슬라이스 리듀서를 통합
25+
// 모든 슬라이스 리듀서를 통합
2526
const rootReducer = combineReducers({
2627
auth: authReducer,
2728
page: pageReducer,
2829
timer: timerReducer,
30+
toast: toastReducer,
2931
});
3032

31-
// persist 적용된 루트 리듀서를 스토어에 설정
33+
// persist 적용된 루트 리듀서를 스토어에 설정
3234
const persistedReducer = persistReducer(persistConfig, rootReducer);
3335

34-
// 스토어 생성 및 미들웨어 설정
36+
// 스토어 생성 및 미들웨어 설정
3537
export const store = configureStore({
3638
reducer: persistedReducer,
3739
middleware: (getDefaultMiddleware) =>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2+
3+
export type ToastType = 'success' | 'warning' | 'information';
4+
5+
interface ToastState {
6+
isShow: boolean;
7+
message: string;
8+
type: ToastType;
9+
}
10+
11+
const initialState: ToastState = {
12+
isShow: false,
13+
message: '',
14+
type: 'success',
15+
};
16+
17+
const toastSlice = createSlice({
18+
name: 'toast',
19+
initialState,
20+
reducers: {
21+
show(state, action: PayloadAction<{ message: string; type: ToastType }>) {
22+
state.isShow = true;
23+
state.message = action.payload.message;
24+
state.type = action.payload.type;
25+
},
26+
hide(state) {
27+
state.isShow = false;
28+
},
29+
},
30+
});
31+
32+
export const { show, hide } = toastSlice.actions;
33+
export default toastSlice.reducer;

react-app/src/styles/index.css

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
}
2020
}
2121

22-
2322
@layer base {
2423
html {
2524
font-family: 'Pretendard-Regular', 'GumiRomanceTTF', sans-serif;
@@ -49,46 +48,44 @@
4948
}
5049
.font-gumi {
5150
font-family: 'GumiRomanceTTF', sans-serif;
52-
53-
}
51+
}
5452

55-
@layer base {
56-
:root {
53+
@layer base {
54+
:root {
55+
--color-primary-base: #f5506c;
56+
--color-primary-hover: #e04562;
57+
--color-primary-light: #ff7b8d;
58+
59+
--color-secondary-base: #6366f1;
60+
--color-secondary-hover: #4f46e5;
61+
--color-secondary-light: #a5b4fc;
5762

58-
--color-primary-base: #f5506c;
59-
--color-primary-hover: #e04562;
60-
--color-primary-light: #ff7b8d;
61-
62-
--color-secondary-base: #6366f1;
63-
--color-secondary-hover: #4f46e5;
64-
--color-secondary-light: #a5b4fc;
65-
66-
--color-lemon: #ffee3d;
67-
--color-soda: #6bd8f9;
68-
--color-peach: #f4bdf8;
69-
--color-orange: #ff933d;
70-
71-
--color-success: #63e383;
72-
--color-warning: #ef6962;
73-
--color-info: #61c3f0;
63+
--color-lemon: #ffee3d;
64+
--color-soda: #6bd8f9;
65+
--color-peach: #f4bdf8;
66+
--color-orange: #ff933d;
7467

75-
--color-gray-200 : #e5e7eb;
68+
--color-success: #3bcb5f;
69+
--color-warning: #ef6962;
70+
--color-info: #61c3f0;
71+
72+
--color-gray-200: #e5e7eb;
73+
}
7674
}
77-
}
7875

79-
.bg-lemon-gradient {
80-
background: linear-gradient(180deg, var(--color-lemon), white);
81-
}
76+
.bg-lemon-gradient {
77+
background: linear-gradient(180deg, var(--color-lemon), white);
78+
}
8279

83-
.bg-soda-gradient {
84-
background: linear-gradient(180deg, var(--color-soda), white);
85-
}
80+
.bg-soda-gradient {
81+
background: linear-gradient(180deg, var(--color-soda), white);
82+
}
8683

87-
.bg-peach-gradient {
88-
background: linear-gradient(180deg, var(--color-peach), white);
89-
}
84+
.bg-peach-gradient {
85+
background: linear-gradient(180deg, var(--color-peach), white);
86+
}
9087

91-
.bg-orange-gradient {
92-
background: linear-gradient(180deg, var(--color-orange), white);
88+
.bg-orange-gradient {
89+
background: linear-gradient(180deg, var(--color-orange), white);
90+
}
9391
}
94-
}

0 commit comments

Comments
 (0)