diff --git a/app/layout.tsx b/app/layout.tsx index c9927ec..0e85c9c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,6 +13,7 @@ import { Geist, Geist_Mono, Outfit } from "next/font/google"; import { QueryProvider } from "@/providers/query-provider"; import { ThemeProvider } from "@/components/theme-provider"; import { SessionManager } from "@/features/auth/components/session-manager"; +import { GlobalAlertModal } from "@/features/layout/components/GlobalAlertModal"; import { Toaster } from "sonner"; import "./globals.css"; @@ -62,6 +63,7 @@ export default function RootLayout({ disableTransitionOnChange > + {children} + + {/* 전역 모달 등록 */} + {children} + + + ); +} +``` + +--- + +## 3. 사용법 (Usage) + +### Hook 가져오기 + +```tsx +import { useGlobalAlert } from "@/features/layout/hooks/use-global-alert"; + +const { alert } = useGlobalAlert(); +``` + +### 기본 알림 (Alert) + +사용자에게 단순히 정보를 전달하고 확인 버튼만 있는 알림입니다. + +```tsx +// 1. 성공 알림 +alert.success("저장이 완료되었습니다."); + +// 2. 에러 알림 +alert.error("데이터 불러오기에 실패했습니다."); + +// 3. 경고 알림 +alert.warning("입력 값이 올바르지 않습니다."); + +// 4. 정보 알림 +alert.info("새로운 버전이 업데이트되었습니다."); +``` + +옵션을 추가하여 제목이나 버튼 텍스트를 변경할 수 있습니다. + +```tsx +alert.success("저장 완료", { + title: "성공", // 기본값: 타입에 따른 제목 (예: "성공", "오류") + confirmLabel: "닫기", // 기본값: "확인" +}); +``` + +### 확인 대화상자 (Confirm) + +사용자의 선택(확인/취소)을 요구하는 대화상자입니다. + +```tsx +alert.confirm("정말로 삭제하시겠습니까?", { + type: "warning", // 기본값: warning (아이콘과 색상 변경됨) + confirmLabel: "삭제", + cancelLabel: "취소", + onConfirm: () => { + console.log("삭제 버튼 클릭됨"); + // 여기에 삭제 로직 추가 + }, + onCancel: () => { + console.log("취소 버튼 클릭됨"); + }, +}); +``` + +--- + +## 4. API Reference + +### `useGlobalAlert()` + +Hook은 `alert` 객체와 `close` 함수를 반환합니다. + +#### `alert` Methods + +| 메서드 | 설명 | 파라미터 | +| --------- | ----------------------- | ---------------------------------------------- | +| `success` | 성공 알림 표시 | `(message: ReactNode, options?: AlertOptions)` | +| `error` | 오류 알림 표시 | `(message: ReactNode, options?: AlertOptions)` | +| `warning` | 경고 알림 표시 | `(message: ReactNode, options?: AlertOptions)` | +| `info` | 정보 알림 표시 | `(message: ReactNode, options?: AlertOptions)` | +| `confirm` | 확인/취소 대화상자 표시 | `(message: ReactNode, options?: AlertOptions)` | + +#### `AlertOptions` Interface + +```typescript +interface AlertOptions { + title?: ReactNode; // 모달 제목 (생략 시 타입에 맞는 기본 제목) + confirmLabel?: string; // 확인 버튼 텍스트 (기본: "확인") + cancelLabel?: string; // 취소 버튼 텍스트 (Confirm 모드에서 기본: "취소") + onConfirm?: () => void; // 확인 버튼 클릭 시 실행될 콜백 + onCancel?: () => void; // 취소 버튼 클릭 시 실행될 콜백 + type?: AlertType; // 알림 타입 ("success" | "error" | "warning" | "info") +} +``` diff --git a/features/layout/components/GlobalAlertModal.tsx b/features/layout/components/GlobalAlertModal.tsx new file mode 100644 index 0000000..ea25358 --- /dev/null +++ b/features/layout/components/GlobalAlertModal.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useGlobalAlertStore } from "@/features/layout/stores/use-global-alert-store"; +import { cn } from "@/lib/utils"; + +export function GlobalAlertModal() { + const { + isOpen, + type, + title, + message, + confirmLabel, + cancelLabel, + onConfirm, + onCancel, + isSingleButton, + closeAlert, + } = useGlobalAlertStore(); + + const handleOpenChange = (open: boolean) => { + if (!open) { + closeAlert(); + } + }; + + const handleConfirm = () => { + onConfirm?.(); + closeAlert(); + }; + + const handleCancel = () => { + onCancel?.(); + closeAlert(); + }; + + const Icon = { + success: CheckCircle2, + error: AlertCircle, + warning: AlertTriangle, + info: Info, + }[type]; + + const iconColor = { + success: "text-emerald-500", + error: "text-red-500", + warning: "text-amber-500", + info: "text-blue-500", + }[type]; + + const bgColor = { + success: "bg-emerald-50 dark:bg-emerald-950/20", + error: "bg-red-50 dark:bg-red-950/20", + warning: "bg-amber-50 dark:bg-amber-950/20", + info: "bg-blue-50 dark:bg-blue-950/20", + }[type]; + + return ( + + + +
+
+ +
+
+ {title} + + {message} + +
+
+
+ + {!isSingleButton && ( + + {cancelLabel || "취소"} + + )} + + {confirmLabel || "확인"} + + +
+
+ ); +} diff --git a/features/layout/hooks/use-global-alert.ts b/features/layout/hooks/use-global-alert.ts new file mode 100644 index 0000000..b4f15ef --- /dev/null +++ b/features/layout/hooks/use-global-alert.ts @@ -0,0 +1,84 @@ +import { ReactNode } from "react"; +import { + AlertType, + useGlobalAlertStore, +} from "@/features/layout/stores/use-global-alert-store"; + +interface AlertOptions { + title?: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + onConfirm?: () => void; + onCancel?: () => void; + type?: AlertType; +} + +export function useGlobalAlert() { + const openAlert = useGlobalAlertStore((state) => state.openAlert); + const closeAlert = useGlobalAlertStore((state) => state.closeAlert); + + const show = ( + message: ReactNode, + type: AlertType = "info", + options?: AlertOptions, + ) => { + openAlert({ + message, + type, + title: options?.title || getDefaultTitle(type), + confirmLabel: options?.confirmLabel || "확인", + cancelLabel: options?.cancelLabel, + onConfirm: options?.onConfirm, + onCancel: options?.onCancel, + isSingleButton: true, + }); + }; + + const confirm = ( + message: ReactNode, + type: AlertType = "warning", + options?: AlertOptions, + ) => { + openAlert({ + message, + type, + title: options?.title || "확인", + confirmLabel: options?.confirmLabel || "확인", + cancelLabel: options?.cancelLabel || "취소", + onConfirm: options?.onConfirm, + onCancel: options?.onCancel, + isSingleButton: false, + }); + }; + + return { + alert: { + success: (message: ReactNode, options?: AlertOptions) => + show(message, "success", options), + warning: (message: ReactNode, options?: AlertOptions) => + show(message, "warning", options), + error: (message: ReactNode, options?: AlertOptions) => + show(message, "error", options), + info: (message: ReactNode, options?: AlertOptions) => + show(message, "info", options), + confirm: (message: ReactNode, options?: AlertOptions) => + confirm(message, options?.type || "warning", options), + }, + close: closeAlert, + }; +} + +function getDefaultTitle(type: AlertType) { + switch (type) { + case "success": + return "성공"; + case "error": + return "오류"; + case "warning": + return "주의"; + case "info": + return "알림"; + default: + return "알림"; + } +} diff --git a/features/layout/stores/use-global-alert-store.ts b/features/layout/stores/use-global-alert-store.ts new file mode 100644 index 0000000..e451421 --- /dev/null +++ b/features/layout/stores/use-global-alert-store.ts @@ -0,0 +1,43 @@ +import { ReactNode } from "react"; +import { create } from "zustand"; + +export type AlertType = "success" | "warning" | "error" | "info"; + +export interface AlertState { + isOpen: boolean; + type: AlertType; + title: ReactNode; + message: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + onConfirm?: () => void; + onCancel?: () => void; + // 단일 버튼 모드 여부 (Confirm 모달이 아닌 단순 Alert) + isSingleButton?: boolean; +} + +interface AlertActions { + openAlert: (params: Omit) => void; + closeAlert: () => void; +} + +const initialState: AlertState = { + isOpen: false, + type: "info", + title: "", + message: "", + confirmLabel: "확인", + cancelLabel: "취소", + isSingleButton: true, +}; + +export const useGlobalAlertStore = create((set) => ({ + ...initialState, + openAlert: (params) => + set({ + ...initialState, // 초기화 후 설정 + ...params, + isOpen: true, + }), + closeAlert: () => set({ isOpen: false }), +}));