[공통컴포넌트] alert 제작
This commit is contained in:
@@ -13,6 +13,7 @@ import { Geist, Geist_Mono, Outfit } from "next/font/google";
|
|||||||
import { QueryProvider } from "@/providers/query-provider";
|
import { QueryProvider } from "@/providers/query-provider";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { SessionManager } from "@/features/auth/components/session-manager";
|
import { SessionManager } from "@/features/auth/components/session-manager";
|
||||||
|
import { GlobalAlertModal } from "@/features/layout/components/GlobalAlertModal";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export default function RootLayout({
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<SessionManager />
|
<SessionManager />
|
||||||
|
<GlobalAlertModal />
|
||||||
<QueryProvider>{children}</QueryProvider>
|
<QueryProvider>{children}</QueryProvider>
|
||||||
<Toaster
|
<Toaster
|
||||||
richColors
|
richColors
|
||||||
|
|||||||
42
common-docs/features/trade-stock-sync.md
Normal file
42
common-docs/features/trade-stock-sync.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Korean Stocks 동기화
|
||||||
|
|
||||||
|
`korean-stocks.json`은 수동 편집 파일이 아니라 자동 생성 파일입니다.
|
||||||
|
직접 수정하지 말고 동기화 스크립트로 갱신하세요.
|
||||||
|
|
||||||
|
## 실행 명령
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run sync:stocks
|
||||||
|
```
|
||||||
|
|
||||||
|
- KIS 최신 KOSPI/KOSDAQ 마스터 파일을 내려받아
|
||||||
|
`features/trade/data/korean-stocks.json`을 다시 생성합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run sync:stocks:check
|
||||||
|
```
|
||||||
|
|
||||||
|
- 현재 파일이 최신인지 검사합니다.
|
||||||
|
- 갱신이 필요하면 종료 코드 `1`로 끝납니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run sync:stocks -- --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
- 원격 파일 파싱/검증만 하고 저장은 하지 않습니다.
|
||||||
|
|
||||||
|
## 권장 운영 방법
|
||||||
|
|
||||||
|
1. 하루 1회(또는 배포 전) `npm run sync:stocks` 실행
|
||||||
|
2. `npm run lint`, `npm run build`로 기본 검증
|
||||||
|
3. 갱신된 `features/trade/data/korean-stocks.json` 커밋
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 데이터 출처:
|
||||||
|
- `https://new.real.download.dws.co.kr/common/master/kospi_code.mst.zip`
|
||||||
|
- `https://new.real.download.dws.co.kr/common/master/kosdaq_code.mst.zip`
|
||||||
|
- 비정상 데이터 저장을 막기 위해 최소 건수 검증(안전장치)을 넣었습니다.
|
||||||
|
- 임시 파일 저장 후 교체(원자적 저장) 방식이라 중간 손상 위험을 줄입니다.
|
||||||
|
- 공식 문서:
|
||||||
|
- `https://apiportal.koreainvestment.com/apiservice-category`
|
||||||
146
common-docs/ui/GLOBAL_ALERT_SYSTEM.md
Normal file
146
common-docs/ui/GLOBAL_ALERT_SYSTEM.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Global Alert System 사용 가이드
|
||||||
|
|
||||||
|
이 문서는 애플리케이션 전역에서 사용 가능한 `Global Alert System`의 설계 목적, 설치 방법, 그리고 사용법을 설명합니다.
|
||||||
|
|
||||||
|
## 1. 개요 (Overview)
|
||||||
|
|
||||||
|
Global Alert System은 Zustand 상태 관리 라이브러리와 Shadcn/ui의 `AlertDialog` 컴포넌트를 결합하여 만든 전역 알림 시스템입니다.
|
||||||
|
복잡한 모달 로직을 매번 구현할 필요 없이, `useGlobalAlert` 훅 하나로 어디서든 일관된 UI의 알림 및 확인 창을 호출할 수 있습니다.
|
||||||
|
|
||||||
|
### 주요 특징
|
||||||
|
|
||||||
|
- **전역 상태 관리**: `useGlobalAlertStore`를 통해 모달의 상태를 중앙에서 관리합니다.
|
||||||
|
- **간편한 Hook**: `useGlobalAlert` 훅을 통해 직관적인 API (`alert.success`, `alert.confirm` 등)를 제공합니다.
|
||||||
|
- **다양한 타입 지원**: Success, Error, Warning, Info 등 상황에 맞는 스타일과 아이콘을 자동으로 적용합니다.
|
||||||
|
- **비동기 지원**: 확인/취소 버튼 클릭 시 콜백 함수를 실행할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 설치 및 설정 (Setup)
|
||||||
|
|
||||||
|
이미 `app/layout.tsx`에 설정되어 있으므로, 개발자는 별도의 설정 없이 바로 사용할 수 있습니다.
|
||||||
|
|
||||||
|
### 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
features/layout/
|
||||||
|
├── components/
|
||||||
|
│ └── GlobalAlertModal.tsx # 실제 렌더링되는 모달 컴포넌트
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-global-alert.ts # 개발자가 사용하는 Custom Hook
|
||||||
|
└── stores/
|
||||||
|
└── use-global-alert-store.ts # Zustand Store
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout 통합
|
||||||
|
|
||||||
|
`app/layout.tsx`에 `GlobalAlertModal`이 이미 등록되어 있습니다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/layout.tsx
|
||||||
|
import { GlobalAlertModal } from "@/features/layout/components/GlobalAlertModal";
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<GlobalAlertModal /> {/* 전역 모달 등록 */}
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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")
|
||||||
|
}
|
||||||
|
```
|
||||||
110
features/layout/components/GlobalAlertModal.tsx
Normal file
110
features/layout/components/GlobalAlertModal.tsx
Normal file
@@ -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 (
|
||||||
|
<AlertDialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<AlertDialogContent className="sm:max-w-[400px]">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-12 w-12 shrink-0 items-center justify-center rounded-full",
|
||||||
|
bgColor,
|
||||||
|
iconColor,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-sm leading-relaxed">
|
||||||
|
{message}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter className="mt-4 sm:justify-end">
|
||||||
|
{!isSingleButton && (
|
||||||
|
<AlertDialogCancel onClick={handleCancel} className="mt-2 sm:mt-0">
|
||||||
|
{cancelLabel || "취소"}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
)}
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className={cn(
|
||||||
|
type === "error" && "bg-red-600 hover:bg-red-700",
|
||||||
|
type === "warning" && "bg-amber-600 hover:bg-amber-700",
|
||||||
|
type === "success" && "bg-emerald-600 hover:bg-emerald-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{confirmLabel || "확인"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
features/layout/hooks/use-global-alert.ts
Normal file
84
features/layout/hooks/use-global-alert.ts
Normal file
@@ -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 "알림";
|
||||||
|
}
|
||||||
|
}
|
||||||
43
features/layout/stores/use-global-alert-store.ts
Normal file
43
features/layout/stores/use-global-alert-store.ts
Normal file
@@ -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<AlertState, "isOpen">) => void;
|
||||||
|
closeAlert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AlertState = {
|
||||||
|
isOpen: false,
|
||||||
|
type: "info",
|
||||||
|
title: "",
|
||||||
|
message: "",
|
||||||
|
confirmLabel: "확인",
|
||||||
|
cancelLabel: "취소",
|
||||||
|
isSingleButton: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGlobalAlertStore = create<AlertState & AlertActions>((set) => ({
|
||||||
|
...initialState,
|
||||||
|
openAlert: (params) =>
|
||||||
|
set({
|
||||||
|
...initialState, // 초기화 후 설정
|
||||||
|
...params,
|
||||||
|
isOpen: true,
|
||||||
|
}),
|
||||||
|
closeAlert: () => set({ isOpen: false }),
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user