diff --git a/src/components/admin/DownloadAdmin.jsx b/src/components/admin/DownloadAdmin.jsx
index b4746fd..0b8d883 100644
--- a/src/components/admin/DownloadAdmin.jsx
+++ b/src/components/admin/DownloadAdmin.jsx
@@ -141,13 +141,40 @@ function UploadForm({ title, hint, accept, endpoint, onSuccess }) {
export default function DownloadAdmin() {
const [info, setInfo] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [loadError, setLoadError] = useState('');
useEffect(() => {
- getDownloadInfo().then(setInfo).catch((err) => {
- console.error('다운로드 정보 로드 실패:', err);
- });
+ getDownloadInfo()
+ .then((data) => {
+ setInfo(data);
+ setLoadError('');
+ })
+ .catch((err) => {
+ console.error('다운로드 정보 로드 실패:', err);
+ setLoadError('배포 정보를 불러올 수 없습니다.');
+ })
+ .finally(() => setLoading(false));
}, []);
+ if (loading) {
+ return (
+
게임 배포 관리
diff --git a/src/components/admin/UserAdmin.jsx b/src/components/admin/UserAdmin.jsx
index 30a88cf..210a72c 100644
--- a/src/components/admin/UserAdmin.jsx
+++ b/src/components/admin/UserAdmin.jsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { getUsers, updateUserRole, deleteUser } from '../../api/users';
import { useAuth } from '../../context/useAuth';
+import { useToast } from '../toast/useToast';
import './AdminCommon.css';
export default function UserAdmin() {
@@ -8,6 +9,7 @@ export default function UserAdmin() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { user: me } = useAuth();
+ const toast = useToast();
const load = useCallback(() => {
getUsers()
@@ -23,9 +25,10 @@ export default function UserAdmin() {
setError('');
try {
await updateUserRole(u.id, newRole);
+ toast.success(`${u.username}의 권한이 ${newRole}로 변경되었습니다.`);
load();
} catch (err) {
- setError(err.message || '권한 변경에 실패했습니다.');
+ toast.error(err.message || '권한 변경에 실패했습니다.');
}
};
@@ -34,9 +37,10 @@ export default function UserAdmin() {
setError('');
try {
await deleteUser(u.id);
+ toast.success(`${u.username} 계정이 삭제되었습니다.`);
load();
} catch (err) {
- setError(err.message || '삭제에 실패했습니다.');
+ toast.error(err.message || '삭제에 실패했습니다.');
}
};
diff --git a/src/components/toast/ToastProvider.jsx b/src/components/toast/ToastProvider.jsx
new file mode 100644
index 0000000..dfa2e70
--- /dev/null
+++ b/src/components/toast/ToastProvider.jsx
@@ -0,0 +1,70 @@
+import { useState, useCallback, useRef } from 'react';
+import { ToastContext } from './toastContextValue';
+
+let toastId = 0;
+
+export function ToastProvider({ children }) {
+ const [toasts, setToasts] = useState([]);
+ const timersRef = useRef({});
+
+ const removeToast = useCallback((id) => {
+ clearTimeout(timersRef.current[id]);
+ delete timersRef.current[id];
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, []);
+
+ const addToast = useCallback((message, type = 'info', duration = 3000) => {
+ const id = ++toastId;
+ setToasts((prev) => [...prev, { id, message, type }]);
+ timersRef.current[id] = setTimeout(() => removeToast(id), duration);
+ return id;
+ }, [removeToast]);
+
+ const toast = useCallback((message) => addToast(message, 'info'), [addToast]);
+ toast.success = useCallback((message) => addToast(message, 'success'), [addToast]);
+ toast.error = useCallback((message) => addToast(message, 'error'), [addToast]);
+ toast.warn = useCallback((message) => addToast(message, 'warn'), [addToast]);
+
+ return (
+
+ {children}
+
+ {toasts.map((t) => (
+
removeToast(t.id)}>
+ {t.message}
+
+ ))}
+
+
+ );
+}
+
+const containerStyle = {
+ position: 'fixed',
+ top: 16,
+ right: 16,
+ zIndex: 10000,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ pointerEvents: 'none',
+};
+
+const itemStyle = {
+ padding: '12px 20px',
+ borderRadius: 8,
+ color: '#fff',
+ fontSize: 14,
+ maxWidth: 360,
+ cursor: 'pointer',
+ pointerEvents: 'auto',
+ boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
+ animation: 'toast-in 0.25s ease-out',
+};
+
+const typeStyles = {
+ info: { backgroundColor: '#3a3a3a' },
+ success: { backgroundColor: '#2d6a4f' },
+ error: { backgroundColor: '#9b2226' },
+ warn: { backgroundColor: '#7f5539' },
+};
diff --git a/src/components/toast/toastContextValue.js b/src/components/toast/toastContextValue.js
new file mode 100644
index 0000000..e61253e
--- /dev/null
+++ b/src/components/toast/toastContextValue.js
@@ -0,0 +1,3 @@
+import { createContext } from 'react';
+
+export const ToastContext = createContext(null);
diff --git a/src/components/toast/useToast.js b/src/components/toast/useToast.js
new file mode 100644
index 0000000..dd6f5c7
--- /dev/null
+++ b/src/components/toast/useToast.js
@@ -0,0 +1,8 @@
+import { useContext } from 'react';
+import { ToastContext } from './toastContextValue';
+
+export function useToast() {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error('useToast must be used within ToastProvider');
+ return ctx;
+}
diff --git a/src/index.css b/src/index.css
index 371c3c3..6f75ecd 100644
--- a/src/index.css
+++ b/src/index.css
@@ -31,3 +31,14 @@ a {
a:hover {
opacity: 0.85;
}
+
+@keyframes toast-in {
+ from {
+ opacity: 0;
+ transform: translateX(40px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}