Skip to content

Commit e776577

Browse files
authored
Merge pull request #61 from DeveloperBlog-Devflow/feature/toast-ui
feat: alert 알림 toast-ui로 개선
2 parents 4414d0f + 856ed99 commit e776577

13 files changed

Lines changed: 273 additions & 83 deletions

File tree

app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next';
22
import './globals.css';
3+
import ToastProvider from '@/components/common/ToastProvider';
34

45
export const metadata: Metadata = {
56
title: 'DevFlow',
@@ -13,7 +14,9 @@ export default function RootLayout({
1314
}) {
1415
return (
1516
<html lang="ko">
16-
<body>{children}</body>
17+
<body>
18+
<ToastProvider>{children}</ToastProvider>
19+
</body>
1720
</html>
1821
);
1922
}

components/auth/FindPasswordModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Button from './Button';
55
import FormField from './FormField';
66
import { auth } from '@/lib/firebase';
77
import { sendPasswordResetEmail } from 'firebase/auth';
8+
import { toast } from 'react-toastify';
89

910
type FindPasswordModalProps = {
1011
onClose: () => void;
@@ -25,17 +26,18 @@ const FindPasswordModal = ({ onClose }: FindPasswordModalProps) => {
2526
e.preventDefault();
2627

2728
if (!email) {
28-
alert('이메일을 입력해주세요.');
29+
toast.info('이메일을 입력해주세요');
2930
return;
3031
}
3132

3233
// 이메일 요청 로직
3334
sendPasswordResetEmail(auth, email)
3435
.then(() => {
35-
alert('이메일이 발송되었습니다.');
36+
toast.success('이메일이 발송되었습니다.');
3637
})
3738
.catch((error) => {
38-
alert('에러 발생: ' + error.message);
39+
console.log('에러 발생: ' + error.message);
40+
toast.error('오류가 발생했습니다.');
3941
});
4042
};
4143

components/auth/SignupForm/SignupForm.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import FormField from '@/components/auth/FormField';
55
import { SignupValues, useSignupForm } from './useSignupForm';
66
import { useRouter } from 'next/navigation';
77
import { signupWithEmail } from '@/services/auth/signup.service';
8+
import { toast } from 'react-toastify';
89

910
const toPayload = (v: SignupValues) => ({
1011
nickname: v.nickname,
@@ -24,7 +25,7 @@ const SignupForm = () => {
2425

2526
try {
2627
await signupWithEmail(toPayload(values));
27-
alert('회원가입 완료되었습니다.');
28+
toast.success('회원가입 완료되었습니다.');
2829
router.push('/login');
2930
} catch (e: unknown) {
3031
const code =
@@ -36,8 +37,8 @@ const SignupForm = () => {
3637
: '';
3738

3839
if (code === 'auth/email-already-in-use')
39-
alert('이미 가입된 이메일입니다');
40-
else alert('회원가입에 실패했습니다');
40+
toast.info('이미 가입된 이메일입니다.');
41+
else toast.error('회원가입에 실패했습니다');
4142
}
4243
};
4344

components/common/Sidebar.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
} from 'lucide-react';
1212
import { signOut } from 'firebase/auth';
1313
import { auth } from '@/lib/firebase';
14+
import { toast } from 'react-toastify';
15+
import { toastConfirm } from '@/utils/toastUtils';
1416

1517
const navItems = [
1618
{ label: '홈', href: '/', icon: Home },
@@ -24,15 +26,22 @@ const Sidebar = () => {
2426
const router = useRouter();
2527

2628
const handleLogout = async (): Promise<void> => {
29+
const ok = await toastConfirm('로그아웃 하시겠습니까?', {
30+
confirmText: '로그아웃',
31+
cancelText: '취소',
32+
});
33+
34+
if (!ok) return;
35+
2736
try {
2837
await signOut(auth);
2938
document.cookie =
3039
'isLoggedIn=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
31-
alert('로그아웃 되었습니다.');
40+
toast.success('로그아웃 되었습니다.');
3241
router.replace('/landing');
3342
} catch (err) {
34-
console.error('로그아웃 실패:', err);
35-
alert('로그아웃에 실패했습니다.');
43+
console.log(err);
44+
toast.error('로그아웃에 실패했습니다.');
3645
}
3746
};
3847

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { ToastContainer } from 'react-toastify';
5+
import 'react-toastify/dist/ReactToastify.css';
6+
7+
const ToastProvider = ({ children }: { children: React.ReactNode }) => {
8+
return (
9+
<>
10+
{children}
11+
<ToastContainer
12+
position="top-right"
13+
autoClose={3000}
14+
closeOnClick
15+
hideProgressBar={false}
16+
pauseOnHover={false}
17+
theme="colored"
18+
/>
19+
</>
20+
);
21+
};
22+
23+
export default ToastProvider;

components/plans/InlineAddPlanForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4+
import { toast } from 'react-toastify';
45

56
interface InlineAddPlanFormProps {
67
onSave: (title: string, description: string) => void;
@@ -13,7 +14,7 @@ const InlineAddPlanForm = ({ onSave, onCancel }: InlineAddPlanFormProps) => {
1314

1415
const handleSave = () => {
1516
if (!title.trim()) {
16-
alert('플랜 제목을 입력해주세요.');
17+
toast.error('플랜 제목을 입력해주세요.');
1718
return;
1819
}
1920
onSave(title, description);

components/plans/InlineAddTaskForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4-
import { Calendar, Plus, X } from 'lucide-react';
4+
import { Plus, X } from 'lucide-react';
55
import DatePicker from './DatePicker';
66

77
interface InlineAddTaskFormProps {

components/plans/PlanSection.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
deletePlanItem,
2323
updatePlanItem,
2424
} from '@/services/plans/planManageService.service';
25+
import { toastConfirm } from '@/utils/toastUtils';
2526

2627
interface PlanSectionProps {
2728
userId: string;
@@ -153,16 +154,24 @@ export default function PlanSection({
153154

154155
// 5. 하위 항목 삭제 핸들러
155156
const handleDeletePlanItem = async (itemId: string, title: string) => {
156-
if (confirm(`'${title}' 하위 항목을 정말 삭제하시겠습니까?`)) {
157-
try {
158-
await deletePlanItem(userId, itemId);
159-
160-
// 목록 새로고침
161-
const fetchedPlanItems = await fetchPlanItems(userId, planId);
162-
setTasks(fetchedPlanItems);
163-
} catch (err) {
164-
console.error(err);
157+
const ok = await toastConfirm(
158+
`${title} 하위 항목을 정말 삭제하시겠습니까?`,
159+
{
160+
confirmText: '삭제',
161+
cancelText: '취소',
165162
}
163+
);
164+
165+
if (!ok) return;
166+
167+
try {
168+
await deletePlanItem(userId, itemId);
169+
170+
// 목록 새로고침
171+
const fetchedPlanItems = await fetchPlanItems(userId, planId);
172+
setTasks(fetchedPlanItems);
173+
} catch (err) {
174+
console.error(err);
166175
}
167176

168177
onChangeStats();

components/tils/TilCard.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { TilItem } from '@/types/til';
55
import { useRouter } from 'next/navigation';
66
import { Menu } from '@headlessui/react';
77
import { useAuthUser } from '@/hooks/useAuthUser';
8+
import { toast } from 'react-toastify';
9+
import { toastConfirm } from '@/utils/toastUtils';
810

911
type Props = {
1012
item: TilItem;
@@ -24,9 +26,15 @@ const TilCard = ({ item, onDelete }: Props) => {
2426
router.push(`/edit/${item.id}`);
2527
};
2628
const onClickDelete = async () => {
27-
if (!confirm('정말 삭제할까요?')) return;
29+
const ok = await toastConfirm('정말 삭제하시겠습니까?', {
30+
confirmText: '삭제',
31+
cancelText: '취소',
32+
});
33+
34+
if (!ok) return;
35+
2836
await onDelete?.(item.id);
29-
alert('성공적으로 삭제되었습니다.');
37+
toast.success('성공적으로 삭제되었습니다.');
3038
};
3139
return (
3240
<article className="relative rounded-3xl border border-black/10 bg-white px-10 py-8">

components/write/Editor.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { createTil, updateTil, fetchMyTil } from '@/services/write/til.service';
88
import { auth } from '@/lib/firebase';
99
import { useRouter } from 'next/navigation';
1010

11+
import { toast } from 'react-toastify';
12+
1113
const MDEditor = dynamic<MDEditorProps>(() => import('@uiw/react-md-editor'), {
1214
ssr: false,
1315
});
@@ -29,7 +31,7 @@ const Editor = ({ tilId }: Props) => {
2931

3032
const user = auth.currentUser;
3133
if (!user) {
32-
alert('로그인이 필요합니다');
34+
toast.error('로그인이 필요합니다.');
3335
router.push('/login');
3436
return;
3537
}
@@ -39,7 +41,7 @@ const Editor = ({ tilId }: Props) => {
3941
setLoading(true);
4042
const til = await fetchMyTil(user.uid, tilId);
4143
if (!til) {
42-
alert('글을 찾을 수 없습니다');
44+
toast.error('글을 찾을 수 없습니다.');
4345
router.back();
4446
return;
4547
}
@@ -48,7 +50,7 @@ const Editor = ({ tilId }: Props) => {
4850
setValue(til.content);
4951
} catch (e) {
5052
console.error(e);
51-
alert('글을 불러오지 못했습니다');
53+
toast.error('글을 불러오지 못했습니다.');
5254
} finally {
5355
setLoading(false);
5456
}
@@ -62,33 +64,35 @@ const Editor = ({ tilId }: Props) => {
6264
const onClickSave = async () => {
6365
const user = auth.currentUser;
6466
if (!user) {
65-
alert('로그인이 필요합니다');
67+
toast.error('로그인이 필요합니다.');
68+
router.push('/login');
6669
return;
6770
}
71+
6872
if (!title.trim()) {
69-
alert('제목을 입력하세요');
73+
toast.error('제목을 입력하세요');
7074
return;
7175
}
7276
if (!value.trim()) {
73-
alert('내용을 입력하세요');
77+
toast.error('내용을 입력하세요');
7478
return;
7579
}
7680

7781
try {
7882
if (isEdit && tilId) {
7983
await updateTil(user.uid, tilId, title, value);
80-
alert('수정 완료!');
84+
toast.success('수정이 완료되었습니다.');
8185
router.push(`/write/${tilId}`); // 상세 페이지로
8286
return;
8387
}
8488
const id = await createTil(user.uid, value, title);
85-
alert('저장 완료!');
86-
console.log('postId:', id);
89+
toast.success('저장이 완료되었습니다.');
90+
// console.log('postId:', id);
8791

8892
router.push(`/write/${id}`);
8993
} catch (e) {
9094
console.error(e);
91-
alert('저장 실패');
95+
toast.error('저장에 실패했습니다.');
9296
}
9397
};
9498

0 commit comments

Comments
 (0)