diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 8aa033b..f31473f 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -32,7 +32,8 @@ export default function LoginForm() { if ('message' in response) { alert(response.message); } else { - alert('로그인이 완료되었습니다!', () => router.replace('/mydashboard')); + await alert('로그인이 완료되었습니다!'); + router.replace('/mydashboard'); } }; diff --git a/src/components/auth/SignupForm.tsx b/src/components/auth/SignupForm.tsx index 2940674..5e9c431 100644 --- a/src/components/auth/SignupForm.tsx +++ b/src/components/auth/SignupForm.tsx @@ -37,7 +37,8 @@ export default function SignupForm() { if ('message' in response) { alert(response.message); } else { - alert('가입이 완료되었습니다!', () => router.replace('/login')); + await alert('가입이 완료되었습니다!'); + router.replace('/login'); } }; diff --git a/src/components/dashboard/DetailDelete.tsx b/src/components/dashboard/DetailDelete.tsx index 4c9a9c6..b659a32 100644 --- a/src/components/dashboard/DetailDelete.tsx +++ b/src/components/dashboard/DetailDelete.tsx @@ -5,14 +5,23 @@ import { useRemoveDashboard } from '@/apis/dashboards/queries'; import Button from '@/components/ui/Button/Button'; import useAlert from '@/hooks/useAlert'; import { getErrorMessage } from '@/utils/errorMessage'; +import useConfirm from '@/hooks/useConfirm'; export default function DetailDelete() { const { id } = useParams<{ id: string }>(); const router = useRouter(); const alert = useAlert(); + const confirm = useConfirm(); const { mutateAsync: remove } = useRemoveDashboard(); const handleDelete = async () => { + const result = await confirm('정말 삭제할까요?', { + buttons: { + ok: '삭제', + }, + }); + if (!result) return; + try { await remove(Number(id)); await alert('삭제했습니다.'); diff --git a/src/components/dashboard/DetailInvited.tsx b/src/components/dashboard/DetailInvited.tsx index 5e11de6..6234f8d 100644 --- a/src/components/dashboard/DetailInvited.tsx +++ b/src/components/dashboard/DetailInvited.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { useCancelInviteDashboard, useDashboardInvitationsQuery } from '@/apis/dashboards/queries'; import { getErrorMessage } from '@/utils/errorMessage'; import useAlert from '@/hooks/useAlert'; +import useConfirm from '@/hooks/useConfirm'; import { Table, TableBody, TableCell, TableCol, TableColGroup, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table/Table'; import PaginationWithCounter from '@/components/pagination/PaginationWithCounter'; import { isAxiosError } from 'axios'; @@ -21,9 +22,13 @@ export default function DetailInvited() { const { data, error, isFetching } = useDashboardInvitationsQuery({ id: Number(id), page, size: PAGE_SIZE }); const { mutateAsync: cancel } = useCancelInviteDashboard(); const alert = useAlert(); + const confirm = useConfirm(); const inviteModalRef = useRef(null); const cancelInvite = async (invitationId: number) => { + const result = await confirm('정말 초대를 취소 할까요?.'); + if (!result) return; + try { await cancel({ dashboardId: Number(id), invitationId }); alert('초대를 취소했습니다.'); diff --git a/src/components/dashboard/DetailMembers.tsx b/src/components/dashboard/DetailMembers.tsx index a38519c..b9df188 100644 --- a/src/components/dashboard/DetailMembers.tsx +++ b/src/components/dashboard/DetailMembers.tsx @@ -4,6 +4,7 @@ import { isAxiosError } from 'axios'; import { useState } from 'react'; import { useParams } from 'next/navigation'; import useAlert from '@/hooks/useAlert'; +import useConfirm from '@/hooks/useConfirm'; import { useMembersQuery, useRemoveMember } from '@/apis/members/queries'; import { getErrorMessage } from '@/utils/errorMessage'; import PaginationWithCounter from '@/components/pagination/PaginationWithCounter'; @@ -20,8 +21,12 @@ export default function DetailMembers() { const { data, error, isFetching } = useMembersQuery({ page, size: PAGE_SIZE, dashboardId: Number(id) }); const { mutateAsync: remove } = useRemoveMember(); const alert = useAlert(); + const confirm = useConfirm(); const removeMember = async (memberId: number) => { + const result = await confirm('정말 맴버를 삭제 할까요?'); + if (!result) return; + try { await remove({ memberId, dashboardId: Number(id) }); alert('맴버를 삭제했습니다.'); diff --git a/src/components/profile/PasswordEdit.tsx b/src/components/profile/PasswordEdit.tsx index 5891ab0..e23a2b2 100644 --- a/src/components/profile/PasswordEdit.tsx +++ b/src/components/profile/PasswordEdit.tsx @@ -38,10 +38,9 @@ export default function PasswordEdit() { } catch (error) { if (isAxiosError(error)) { if (error?.status === 401) { - alert('세션이 만료되어 로그인 페이지로 이동합니다.', async () => { - await logout(); - router.replace('/login'); - }); + await alert('세션이 만료되어 로그인 페이지로 이동합니다.'); + await logout(); + router.replace('/login'); return; } diff --git a/src/components/ui/Modal/DialogContainer.tsx b/src/components/ui/Modal/DialogContainer.tsx index 25ed5db..b19e876 100644 --- a/src/components/ui/Modal/DialogContainer.tsx +++ b/src/components/ui/Modal/DialogContainer.tsx @@ -1,15 +1,19 @@ 'use client'; import { useDialogStore } from '@/stores/modalStore'; -import BaseModal from './BaseModal'; -import Button from '../Button/Button'; +import BaseModal from '@/components/ui/Modal/BaseModal'; +import Button from '@/components/ui/Button/Button'; export function DialogContainer() { - const { isOpen, message, callback, closeDialog } = useDialogStore(); + const { isOpen, message, type, buttons, callback, resolvePromise, rejectPromise } = useDialogStore(); const handleConfirm = () => { callback?.(); - closeDialog(); + resolvePromise?.(); + }; + + const handleCancel = () => { + rejectPromise?.(); }; return ( @@ -17,8 +21,13 @@ export function DialogContainer() {
{message}
+ {type === 'confirm' && ( + + )}
diff --git a/src/hooks/useAlert.ts b/src/hooks/useAlert.ts index 05da0fc..72fb15c 100644 --- a/src/hooks/useAlert.ts +++ b/src/hooks/useAlert.ts @@ -1,9 +1,11 @@ -import { useDialogStore } from '@/stores/modalStore'; +import { DialogButtonsType, useDialogStore } from '@/stores/modalStore'; /** * useAlert 훅 * - * 간단한 메시지 노출과 선택적 콜백을 실행해주는 알럿 다이얼로그 + * 간단한 알럿 다이얼로그를 띄워 메시지를 표시하고, + * 콜백 실행이나 `await`로 다이얼로그 닫힘을 기다릴 수 있습니다. + * 확인 버튼의 텍스트도 원하는 대로 커스터마이징 가능합니다. * * @example * import useAlert from '@/hooks/useAlert'; @@ -11,21 +13,32 @@ import { useDialogStore } from '@/stores/modalStore'; * function Component() { * const alert = useAlert(); * - * const handleClick = () => { - * alert('메시지 표시', () => { - * console.log('다이얼로그가 닫힌후 콜백입니다. 생략 가능합니다.'); + * const handleClick = async () => { + * // 다이얼로그가 닫힐 때까지 기다림 + * await alert('메시지 표시', { + * callback: () => { + * console.log('다이얼로그가 닫힌 후 실행되는 콜백입니다. 생략 가능합니다.'); + * }, + * buttons: { ok: '확인' }, // 확인 버튼 이름을 커스터마이징 * }); + * + * console.log('다이얼로그가 닫힌 후 추가 작업을 실행합니다.'); * }; * * return ; * } */ +type AlertOptionsType = { + buttons: Partial>; + callback: () => void; +}; + export default function useAlert() { const { openDialog } = useDialogStore(); - const alert = async (message: string, callback?: () => void) => { - await openDialog({ message, callback }); + const alert = async (message: string, options?: Partial) => { + return openDialog({ message, type: 'alert', ...options }); }; return alert; diff --git a/src/hooks/useConfirm.ts b/src/hooks/useConfirm.ts new file mode 100644 index 0000000..87bbbf8 --- /dev/null +++ b/src/hooks/useConfirm.ts @@ -0,0 +1,52 @@ +import { DialogButtonsType, useDialogStore } from '@/stores/modalStore'; + +/** + * useConfirm 훅 + * + * 확인과 취소 버튼이 있는 다이얼로그를 띄워 사용자에게 동작 여부를 묻습니다. + * 버튼 텍스트를 커스터마이징할 수 있으며, `await`로 사용자 응답을 기다릴 수 있습니다. + * + * @example + * import useConfirm from '@/hooks/useConfirm'; + * + * function Component() { + * const confirm = useConfirm(); + * + * const handleClick = async () => { + * // 다이얼로그가 닫힐 때까지 기다림 + * const userConfirmed = await confirm('정말로 삭제하시겠습니까?', { + * buttons: { + * ok: '삭제', + * cancel: '취소', + * }, + * callback: () => { + * console.log('다이얼로그가 닫힌 후 실행되는 콜백입니다. 생략 가능합니다.'); + * }, + * }); + * + * if (userConfirmed) { + * console.log('사용자가 확인 버튼을 눌렀습니다.'); + * } else { + * console.log('사용자가 취소 버튼을 눌렀습니다.'); + * } + * }; + * + * return ; + * } + + */ + +type ConfirmOptionsType = { + buttons: Partial; + callback: () => void; +}; + +export default function useConfirm() { + const { openDialog } = useDialogStore(); + + const confirm = async (message: string, options?: Partial) => { + return openDialog({ message, type: 'confirm', ...options }); + }; + + return confirm; +} diff --git a/src/stores/modalStore.ts b/src/stores/modalStore.ts index b1cd0b6..9c8b51e 100644 --- a/src/stores/modalStore.ts +++ b/src/stores/modalStore.ts @@ -1,25 +1,58 @@ import { create } from 'zustand'; -type DialogState = { +export type DialogButtonsType = { + ok: string; + cancel: string; +}; +export type DialogType = 'alert' | 'confirm'; +export type DialogState = { isOpen: boolean; message: string; + type?: DialogType; + buttons: DialogButtonsType; callback?: () => void; resolvePromise?: () => void; - - openDialog: ({ message, callback }: { message: string; callback?: () => void }) => Promise; - closeDialog: () => void; + rejectPromise?: () => void; + openDialog: ({ message, type, buttons, callback }: { message: string; type: DialogType; buttons?: Partial; callback?: () => void }) => Promise; }; -export const useDialogStore = create()((set) => ({ - isOpen: false, - message: '', - callback: undefined, - resolvePromise: undefined, +export const useDialogStore = create()((set) => { + const defaultButtons: DialogButtonsType = { + ok: '확인', + cancel: '취소', + }; + + return { + isOpen: false, + message: '', + type: undefined, + buttons: defaultButtons, + callback: undefined, + resolvePromise: undefined, + rejectPromise: undefined, + openDialog: ({ message, callback, type, buttons }) => { + const promise = new Promise((resolve, reject) => { + return set({ + isOpen: true, + message, + type, + buttons: { ...defaultButtons, ...buttons }, + callback, + resolvePromise: resolve, + rejectPromise: reject, + }); + }); - openDialog: ({ message, callback }) => new Promise((resolve) => set({ isOpen: true, message, callback, resolvePromise: resolve })), - closeDialog: () => - set((state) => { - state.resolvePromise?.(); - return { isOpen: false, message: '', callback: undefined, resolvePromise: undefined }; - }), -})); + return promise.then( + () => { + set({ isOpen: false, message: '', callback: undefined, resolvePromise: undefined, type: undefined }); + return true; + }, + () => { + set({ isOpen: false, message: '', callback: undefined, resolvePromise: undefined, type: undefined }); + return false; + }, + ); + }, + }; +});