Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/components/dashboard/DetailDelete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('삭제했습니다.');
Expand Down
5 changes: 5 additions & 0 deletions src/components/dashboard/DetailInvited.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<ModalHandle | null>(null);

const cancelInvite = async (invitationId: number) => {
const result = await confirm('정말 초대를 취소 할까요?.');
if (!result) return;

try {
await cancel({ dashboardId: Number(id), invitationId });
alert('초대를 취소했습니다.');
Expand Down
5 changes: 5 additions & 0 deletions src/components/dashboard/DetailMembers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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('맴버를 삭제했습니다.');
Expand Down
7 changes: 3 additions & 4 deletions src/components/profile/PasswordEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
19 changes: 14 additions & 5 deletions src/components/ui/Modal/DialogContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
'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 (
<BaseModal isOpen={isOpen}>
<div className='w-dvw max-w-[272px] rounded-lg bg-white px-4 py-6 md:max-w-[368px] md:px-6'>
<div className='break-keep text-center text-xl font-medium'>{message}</div>
<div className='mt-10 flex justify-center gap-2'>
{type === 'confirm' && (
<Button size='sm' variant='outline' onClick={handleCancel}>
{buttons.cancel}
</Button>
)}
<Button size='sm' onClick={handleConfirm}>
확인
{buttons.ok}
</Button>
</div>
</div>
Expand Down
27 changes: 20 additions & 7 deletions src/hooks/useAlert.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
import { useDialogStore } from '@/stores/modalStore';
import { DialogButtonsType, useDialogStore } from '@/stores/modalStore';

/**
* useAlert 훅
*
* 간단한 메시지 노출과 선택적 콜백을 실행해주는 알럿 다이얼로그
* 간단한 알럿 다이얼로그를 띄워 메시지를 표시하고,
* 콜백 실행이나 `await`로 다이얼로그 닫힘을 기다릴 수 있습니다.
* 확인 버튼의 텍스트도 원하는 대로 커스터마이징 가능합니다.
*
* @example
* import useAlert from '@/hooks/useAlert';
*
* function Component() {
* const alert = useAlert();
*
* const handleClick = () => {
* alert('메시지 표시', () => {
* console.log('다이얼로그가 닫힌후 콜백입니다. 생략 가능합니다.');
* const handleClick = async () => {
* // 다이얼로그가 닫힐 때까지 기다림
* await alert('메시지 표시', {
* callback: () => {
* console.log('다이얼로그가 닫힌 후 실행되는 콜백입니다. 생략 가능합니다.');
* },
* buttons: { ok: '확인' }, // 확인 버튼 이름을 커스터마이징
* });
*
* console.log('다이얼로그가 닫힌 후 추가 작업을 실행합니다.');
* };
*
* return <button onClick={handleClick}>알림 표시</button>;
* }
*/

type AlertOptionsType = {
buttons: Partial<Pick<DialogButtonsType, 'ok'>>;
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<AlertOptionsType>) => {
return openDialog({ message, type: 'alert', ...options });
};

return alert;
Expand Down
52 changes: 52 additions & 0 deletions src/hooks/useConfirm.ts
Original file line number Diff line number Diff line change
@@ -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 <button onClick={handleClick}>삭제하기</button>;
* }

*/

type ConfirmOptionsType = {
buttons: Partial<DialogButtonsType>;
callback: () => void;
};

export default function useConfirm() {
const { openDialog } = useDialogStore();

const confirm = async (message: string, options?: Partial<ConfirmOptionsType>) => {
return openDialog({ message, type: 'confirm', ...options });
};

return confirm;
}
65 changes: 49 additions & 16 deletions src/stores/modalStore.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
closeDialog: () => void;
rejectPromise?: () => void;
openDialog: ({ message, type, buttons, callback }: { message: string; type: DialogType; buttons?: Partial<DialogButtonsType>; callback?: () => void }) => Promise<boolean>;
};

export const useDialogStore = create<DialogState>()((set) => ({
isOpen: false,
message: '',
callback: undefined,
resolvePromise: undefined,
export const useDialogStore = create<DialogState>()((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<void>((resolve, reject) => {
return set({
isOpen: true,
message,
type,
buttons: { ...defaultButtons, ...buttons },
callback,
resolvePromise: resolve,
rejectPromise: reject,
});
});

openDialog: ({ message, callback }) => new Promise<void>((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;
},
);
},
};
});