diff --git a/adminfront/package-lock.json b/adminfront/package-lock.json
index 484e6408..524f770a 100644
--- a/adminfront/package-lock.json
+++ b/adminfront/package-lock.json
@@ -26,7 +26,6 @@
"react-hook-form": "^7.71.1",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.28.2",
- "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zod": "^3.24.1"
},
@@ -5156,16 +5155,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/sonner": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
- "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
- "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
diff --git a/adminfront/package.json b/adminfront/package.json
index 0ff96d03..0cbcd1fc 100644
--- a/adminfront/package.json
+++ b/adminfront/package.json
@@ -34,7 +34,6 @@
"react-hook-form": "^7.71.1",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.28.2",
- "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zod": "^3.24.1"
},
diff --git a/adminfront/src/components/ui/toaster.tsx b/adminfront/src/components/ui/toaster.tsx
new file mode 100644
index 00000000..864901c9
--- /dev/null
+++ b/adminfront/src/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
+import { AlertCircle, CheckCircle2, Info } from "lucide-react";
+import { cn } from "../../lib/utils";
+import { useToastState } from "./use-toast";
+
+export function Toaster() {
+ const toasts = useToastState();
+
+ if (toasts.length === 0) return null;
+
+ return (
+
+ {toasts.map((t) => (
+
+ {t.type === "success" && (
+
+ )}
+ {t.type === "error" &&
}
+ {t.type === "info" &&
}
+
{t.message}
+
+ ))}
+
+ );
+}
diff --git a/adminfront/src/components/ui/use-toast.ts b/adminfront/src/components/ui/use-toast.ts
new file mode 100644
index 00000000..adbc7f38
--- /dev/null
+++ b/adminfront/src/components/ui/use-toast.ts
@@ -0,0 +1,60 @@
+import * as React from "react";
+
+type ToastType = "success" | "error" | "info";
+
+interface Toast {
+ id: string;
+ message: string;
+ type: ToastType;
+}
+
+let subscribers: ((toasts: Toast[]) => void)[] = [];
+let toasts: Toast[] = [];
+
+const notify = () => {
+ for (const sub of subscribers) {
+ sub(toasts);
+ }
+};
+
+const toastBase = (message: string, type: ToastType = "success") => {
+ const id = Math.random().toString(36).substring(2, 9);
+ toasts = [...toasts, { id, message, type }];
+ notify();
+
+ setTimeout(() => {
+ toasts = toasts.filter((t) => t.id !== id);
+ notify();
+ }, 3000);
+};
+
+export const toast = Object.assign(toastBase, {
+ success: (message: string, options?: { description?: string }) => {
+ const finalMessage = options?.description ? `${message}
+${options.description}` : message;
+ toastBase(finalMessage, "success");
+ },
+ error: (message: string, options?: { description?: string }) => {
+ const finalMessage = options?.description ? `${message}
+${options.description}` : message;
+ toastBase(finalMessage, "error");
+ },
+ info: (message: string, options?: { description?: string }) => {
+ const finalMessage = options?.description ? `${message}
+${options.description}` : message;
+ toastBase(finalMessage, "info");
+ }
+});
+
+export const useToastState = () => {
+ const [state, setState] = React.useState(toasts);
+
+ React.useEffect(() => {
+ subscribers.push(setState);
+ return () => {
+ subscribers = subscribers.filter((sub) => sub !== setState);
+ };
+ }, []);
+
+ return state;
+};
diff --git a/adminfront/src/features/tenants/components/OrgChartUploadModal.tsx b/adminfront/src/features/tenants/components/OrgChartUploadModal.tsx
index ad7b27eb..1a993c18 100644
--- a/adminfront/src/features/tenants/components/OrgChartUploadModal.tsx
+++ b/adminfront/src/features/tenants/components/OrgChartUploadModal.tsx
@@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Download, FileText, Loader2, Upload } from "lucide-react";
import * as React from "react";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Dialog,
diff --git a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx
index 33faca35..5186823c 100644
--- a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx
+++ b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx
@@ -12,7 +12,7 @@ import {
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import { useParams } from "react-router-dom";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
diff --git a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx
index 511f6680..6d56c737 100644
--- a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx
@@ -18,7 +18,7 @@ import {
import type React from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
diff --git a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx
index aba6f9d5..17b787c3 100644
--- a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx
@@ -3,7 +3,7 @@ import type { AxiosError } from "axios";
import { Save, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Card,
diff --git a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
index 221c9a90..c3c9f1d5 100644
--- a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx
@@ -3,7 +3,7 @@ import type { AxiosError } from "axios";
import { Plus, Save, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Card,
diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx
index ad60e0d3..d7f73e79 100644
--- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx
+++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx
@@ -20,7 +20,7 @@ import {
import * as React from "react";
import { useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
-import { toast } from "sonner";
+import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -266,7 +266,7 @@ const MemberTable: React.FC<{
{t("msg.admin.users.list.empty", "멤버가 없습니다.")}