refactor: useToast 커스텀 훅으로 Toast 로직 통합

- hooks/useToast.js 생성 (44줄)
- 적용 파일 (9개):
  - AdminMembers.jsx
  - AdminMemberEdit.jsx
  - AdminAlbums.jsx
  - AdminAlbumForm.jsx
  - AdminAlbumPhotos.jsx
  - AdminSchedule.jsx
  - AdminScheduleForm.jsx
  - AdminScheduleBots.jsx
  - AdminScheduleCategory.jsx
- 각 파일에서 중복된 toast useState/useEffect 제거
- showSuccess/showError 편의 메서드 활용

총 약 70줄의 중복 코드 제거
This commit is contained in:
caadiq 2026-01-09 22:57:34 +09:00
parent 3367f4806d
commit 0b00055773
10 changed files with 81 additions and 86 deletions

View file

@ -0,0 +1,56 @@
/**
* Toast 상태 관리 커스텀
* 자동 숨김 타이머 상태 관리를 제공
*/
import { useState, useEffect, useCallback } from "react";
function useToast(duration = 3000) {
const [toast, setToast] = useState(null);
// Toast 자동 숨김
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), duration);
return () => clearTimeout(timer);
}
}, [toast, duration]);
// Toast 표시 함수
const showToast = useCallback((message, type = "info") => {
setToast({ message, type });
}, []);
// 편의 메서드
const showSuccess = useCallback(
(message) => showToast(message, "success"),
[showToast]
);
const showError = useCallback(
(message) => showToast(message, "error"),
[showToast]
);
const showWarning = useCallback(
(message) => showToast(message, "warning"),
[showToast]
);
const showInfo = useCallback(
(message) => showToast(message, "info"),
[showToast]
);
// Toast 숨김 함수
const hideToast = useCallback(() => setToast(null), []);
return {
toast,
setToast,
showToast,
showSuccess,
showError,
showWarning,
showInfo,
hideToast,
};
}
export default useToast;

View file

@ -7,6 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import CustomDatePicker from '../../../components/admin/CustomDatePicker'; import CustomDatePicker from '../../../components/admin/CustomDatePicker';
import useToast from '../../../hooks/useToast';
// //
function CustomSelect({ value, onChange, options, placeholder }) { function CustomSelect({ value, onChange, options, placeholder }) {
@ -83,15 +84,7 @@ function AdminAlbumForm() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [coverPreview, setCoverPreview] = useState(null); const [coverPreview, setCoverPreview] = useState(null);
const [coverFile, setCoverFile] = useState(null); const [coverFile, setCoverFile] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '', title: '',

View file

@ -8,6 +8,7 @@ import {
Tag, FolderOpen, Save Tag, FolderOpen, Save
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import { getAlbum } from '../../../api/public/albums'; import { getAlbum } from '../../../api/public/albums';
import { getMembers } from '../../../api/public/members'; import { getMembers } from '../../../api/public/members';
@ -24,7 +25,7 @@ function AdminAlbumPhotos() {
const [teasers, setTeasers] = useState([]); // const [teasers, setTeasers] = useState([]); //
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const [selectedPhotos, setSelectedPhotos] = useState([]); const [selectedPhotos, setSelectedPhotos] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
@ -139,14 +140,6 @@ function AdminAlbumPhotos() {
})); }));
}; };
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
// //
if (!authApi.hasToken()) { if (!authApi.hasToken()) {

View file

@ -7,6 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import { getAlbums } from '../../../api/public/albums'; import { getAlbums } from '../../../api/public/albums';
import * as albumsApi from '../../../api/admin/albums'; import * as albumsApi from '../../../api/admin/albums';
@ -17,18 +18,10 @@ function AdminAlbums() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const [deleteDialog, setDeleteDialog] = useState({ show: false, album: null }); const [deleteDialog, setDeleteDialog] = useState({ show: false, album: null });
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
// //
if (!authApi.hasToken()) { if (!authApi.hasToken()) {

View file

@ -7,6 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import CustomDatePicker from '../../../components/admin/CustomDatePicker'; import CustomDatePicker from '../../../components/admin/CustomDatePicker';
import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import * as membersApi from '../../../api/admin/members'; import * as membersApi from '../../../api/admin/members';
@ -16,7 +17,7 @@ function AdminMemberEdit() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const [imagePreview, setImagePreview] = useState(null); const [imagePreview, setImagePreview] = useState(null);
const [imageFile, setImageFile] = useState(null); const [imageFile, setImageFile] = useState(null);
@ -28,14 +29,6 @@ function AdminMemberEdit() {
is_former: false is_former: false
}); });
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
// //
if (!authApi.hasToken()) { if (!authApi.hasToken()) {

View file

@ -6,21 +6,14 @@ import {
Home, ChevronRight, Users, User Home, ChevronRight, Users, User
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import useToast from '../../../hooks/useToast';
function AdminMembers() { function AdminMembers() {
const navigate = useNavigate(); const navigate = useNavigate();
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
// //

View file

@ -11,6 +11,7 @@ import { useInView } from 'react-intersection-observer';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import useScheduleStore from '../../../stores/useScheduleStore'; import useScheduleStore from '../../../stores/useScheduleStore';
import useToast from '../../../hooks/useToast';
import { getTodayKST, formatDate } from '../../../utils/date'; import { getTodayKST, formatDate } from '../../../utils/date';
import * as schedulesApi from '../../../api/admin/schedules'; import * as schedulesApi from '../../../api/admin/schedules';
import * as categoriesApi from '../../../api/admin/categories'; import * as categoriesApi from '../../../api/admin/categories';
@ -138,7 +139,7 @@ function AdminSchedule() {
// ( ) // ( )
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const scrollContainerRef = useRef(null); const scrollContainerRef = useRef(null);
const SEARCH_LIMIT = 5; // 5 const SEARCH_LIMIT = 5; // 5
@ -304,14 +305,6 @@ function AdminSchedule() {
return cat?.color || '#4A7C59'; return cat?.color || '#4A7C59';
}; };
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('adminToken'); const token = localStorage.getItem('adminToken');
const userData = localStorage.getItem('adminUser'); const userData = localStorage.getItem('adminUser');

View file

@ -7,26 +7,19 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import useToast from '../../../hooks/useToast';
import * as botsApi from '../../../api/admin/bots'; import * as botsApi from '../../../api/admin/bots';
function AdminScheduleBots() { function AdminScheduleBots() {
const navigate = useNavigate(); const navigate = useNavigate();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const [bots, setBots] = useState([]); const [bots, setBots] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isInitialLoad, setIsInitialLoad] = useState(true); // () const [isInitialLoad, setIsInitialLoad] = useState(true); // ()
const [syncing, setSyncing] = useState(null); // ID const [syncing, setSyncing] = useState(null); // ID
const [quotaWarning, setQuotaWarning] = useState(null); // const [quotaWarning, setQuotaWarning] = useState(null); //
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('adminToken'); const token = localStorage.getItem('adminToken');
const userData = localStorage.getItem('adminUser'); const userData = localStorage.getItem('adminUser');

View file

@ -4,6 +4,7 @@ import { motion, AnimatePresence, Reorder } from 'framer-motion';
import { LogOut, Home, ChevronRight, Plus, Edit3, Trash2, GripVertical, X, AlertTriangle } from 'lucide-react'; import { LogOut, Home, ChevronRight, Plus, Edit3, Trash2, GripVertical, X, AlertTriangle } from 'lucide-react';
import { HexColorPicker } from 'react-colorful'; import { HexColorPicker } from 'react-colorful';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import * as categoriesApi from '../../../api/admin/categories'; import * as categoriesApi from '../../../api/admin/categories';
@ -38,13 +39,7 @@ function AdminScheduleCategory() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [toast, setToast] = useState(null); const { toast, setToast, showSuccess, showError } = useToast();
// (3 )
const showToast = (type, message) => {
setToast({ type, message });
setTimeout(() => setToast(null), 3000);
};
// //
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@ -84,7 +79,7 @@ function AdminScheduleCategory() {
setCategories(data); setCategories(data);
} catch (error) { } catch (error) {
console.error('카테고리 조회 오류:', error); console.error('카테고리 조회 오류:', error);
showToast('error', '카테고리를 불러오는데 실패했습니다.'); showError('카테고리를 불러오는데 실패했습니다.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -112,7 +107,7 @@ function AdminScheduleCategory() {
// //
const handleSave = async () => { const handleSave = async () => {
if (!formData.name.trim()) { if (!formData.name.trim()) {
showToast('error', '카테고리 이름을 입력해주세요.'); showError('카테고리 이름을 입력해주세요.');
return; return;
} }
@ -122,7 +117,7 @@ function AdminScheduleCategory() {
&& cat.id !== editingCategory?.id && cat.id !== editingCategory?.id
); );
if (isDuplicate) { if (isDuplicate) {
showToast('error', '이미 존재하는 카테고리입니다.'); showError('이미 존재하는 카테고리입니다.');
return; return;
} }
@ -132,12 +127,12 @@ function AdminScheduleCategory() {
} else { } else {
await categoriesApi.createCategory(formData); await categoriesApi.createCategory(formData);
} }
showToast('success', editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.'); showSuccess(editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.');
setModalOpen(false); setModalOpen(false);
fetchCategories(); fetchCategories();
} catch (error) { } catch (error) {
console.error('저장 오류:', error); console.error('저장 오류:', error);
showToast('error', error.message || '저장에 실패했습니다.'); showError(error.message || '저장에 실패했습니다.');
} }
}; };
@ -153,13 +148,13 @@ function AdminScheduleCategory() {
try { try {
await categoriesApi.deleteCategory(deleteTarget.id); await categoriesApi.deleteCategory(deleteTarget.id);
showToast('success', '카테고리가 삭제되었습니다.'); showSuccess('카테고리가 삭제되었습니다.');
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setDeleteTarget(null); setDeleteTarget(null);
fetchCategories(); fetchCategories();
} catch (error) { } catch (error) {
console.error('삭제 오류:', error); console.error('삭제 오류:', error);
showToast('error', error.message || '삭제에 실패했습니다.'); showError(error.message || '삭제에 실패했습니다.');
} }
}; };

View file

@ -27,6 +27,7 @@ import {
import Toast from "../../../components/Toast"; import Toast from "../../../components/Toast";
import Lightbox from "../../../components/common/Lightbox"; import Lightbox from "../../../components/common/Lightbox";
import CustomDatePicker from "../../../components/admin/CustomDatePicker"; import CustomDatePicker from "../../../components/admin/CustomDatePicker";
import useToast from "../../../hooks/useToast";
import * as authApi from "../../../api/admin/auth"; import * as authApi from "../../../api/admin/auth";
import * as categoriesApi from "../../../api/admin/categories"; import * as categoriesApi from "../../../api/admin/categories";
import * as schedulesApi from "../../../api/admin/schedules"; import * as schedulesApi from "../../../api/admin/schedules";
@ -401,7 +402,7 @@ function AdminScheduleForm() {
const isEditMode = !!id; const isEditMode = !!id;
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [toast, setToast] = useState(null); const { toast, setToast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
@ -507,14 +508,6 @@ function AdminScheduleForm() {
} }
}; };
// Toast
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => { useEffect(() => {
if (!authApi.hasToken()) { if (!authApi.hasToken()) {
navigate("/admin"); navigate("/admin");