From 0b00055773959cea8615e0b65f57664999e6045f Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 9 Jan 2026 22:57:34 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20useToast=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20Toast=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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줄의 중복 코드 제거 --- frontend/src/hooks/useToast.js | 56 +++++++++++++++++++ .../src/pages/pc/admin/AdminAlbumForm.jsx | 11 +--- .../src/pages/pc/admin/AdminAlbumPhotos.jsx | 11 +--- frontend/src/pages/pc/admin/AdminAlbums.jsx | 11 +--- .../src/pages/pc/admin/AdminMemberEdit.jsx | 11 +--- frontend/src/pages/pc/admin/AdminMembers.jsx | 11 +--- frontend/src/pages/pc/admin/AdminSchedule.jsx | 11 +--- .../src/pages/pc/admin/AdminScheduleBots.jsx | 11 +--- .../pages/pc/admin/AdminScheduleCategory.jsx | 23 +++----- .../src/pages/pc/admin/AdminScheduleForm.jsx | 11 +--- 10 files changed, 81 insertions(+), 86 deletions(-) create mode 100644 frontend/src/hooks/useToast.js diff --git a/frontend/src/hooks/useToast.js b/frontend/src/hooks/useToast.js new file mode 100644 index 0000000..de39f46 --- /dev/null +++ b/frontend/src/hooks/useToast.js @@ -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; diff --git a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx index e19ddff..e1c2f40 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx @@ -7,6 +7,7 @@ import { } from 'lucide-react'; import Toast from '../../../components/Toast'; import CustomDatePicker from '../../../components/admin/CustomDatePicker'; +import useToast from '../../../hooks/useToast'; // 커스텀 드롭다운 컴포넌트 function CustomSelect({ value, onChange, options, placeholder }) { @@ -83,15 +84,7 @@ function AdminAlbumForm() { const [saving, setSaving] = useState(false); const [coverPreview, setCoverPreview] = useState(null); const [coverFile, setCoverFile] = useState(null); - const [toast, setToast] = useState(null); - - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); + const { toast, setToast } = useToast(); const [formData, setFormData] = useState({ title: '', diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx index 577d4f2..6a4799d 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx @@ -8,6 +8,7 @@ import { Tag, FolderOpen, Save } from 'lucide-react'; import Toast from '../../../components/Toast'; +import useToast from '../../../hooks/useToast'; import * as authApi from '../../../api/admin/auth'; import { getAlbum } from '../../../api/public/albums'; import { getMembers } from '../../../api/public/members'; @@ -24,7 +25,7 @@ function AdminAlbumPhotos() { const [teasers, setTeasers] = useState([]); // 티저 이미지 const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const [selectedPhotos, setSelectedPhotos] = useState([]); const [uploading, setUploading] = useState(false); 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(() => { // 로그인 확인 if (!authApi.hasToken()) { diff --git a/frontend/src/pages/pc/admin/AdminAlbums.jsx b/frontend/src/pages/pc/admin/AdminAlbums.jsx index 4ea944c..b233d8f 100644 --- a/frontend/src/pages/pc/admin/AdminAlbums.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbums.jsx @@ -7,6 +7,7 @@ import { } from 'lucide-react'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; +import useToast from '../../../hooks/useToast'; import * as authApi from '../../../api/admin/auth'; import { getAlbums } from '../../../api/public/albums'; import * as albumsApi from '../../../api/admin/albums'; @@ -17,18 +18,10 @@ function AdminAlbums() { const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const [deleteDialog, setDeleteDialog] = useState({ show: false, album: null }); const [deleting, setDeleting] = useState(false); - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); - useEffect(() => { // 로그인 확인 if (!authApi.hasToken()) { diff --git a/frontend/src/pages/pc/admin/AdminMemberEdit.jsx b/frontend/src/pages/pc/admin/AdminMemberEdit.jsx index a98a34d..d8d098f 100644 --- a/frontend/src/pages/pc/admin/AdminMemberEdit.jsx +++ b/frontend/src/pages/pc/admin/AdminMemberEdit.jsx @@ -7,6 +7,7 @@ import { } from 'lucide-react'; import Toast from '../../../components/Toast'; import CustomDatePicker from '../../../components/admin/CustomDatePicker'; +import useToast from '../../../hooks/useToast'; import * as authApi from '../../../api/admin/auth'; import * as membersApi from '../../../api/admin/members'; @@ -16,7 +17,7 @@ function AdminMemberEdit() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const [imagePreview, setImagePreview] = useState(null); const [imageFile, setImageFile] = useState(null); @@ -28,14 +29,6 @@ function AdminMemberEdit() { is_former: false }); - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); - useEffect(() => { // 로그인 확인 if (!authApi.hasToken()) { diff --git a/frontend/src/pages/pc/admin/AdminMembers.jsx b/frontend/src/pages/pc/admin/AdminMembers.jsx index 2cf089f..f1d335f 100644 --- a/frontend/src/pages/pc/admin/AdminMembers.jsx +++ b/frontend/src/pages/pc/admin/AdminMembers.jsx @@ -6,21 +6,14 @@ import { Home, ChevronRight, Users, User } from 'lucide-react'; import Toast from '../../../components/Toast'; +import useToast from '../../../hooks/useToast'; function AdminMembers() { const navigate = useNavigate(); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); - - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); + const { toast, setToast } = useToast(); useEffect(() => { // 로그인 확인 diff --git a/frontend/src/pages/pc/admin/AdminSchedule.jsx b/frontend/src/pages/pc/admin/AdminSchedule.jsx index dc5c56b..28a1ee2 100644 --- a/frontend/src/pages/pc/admin/AdminSchedule.jsx +++ b/frontend/src/pages/pc/admin/AdminSchedule.jsx @@ -11,6 +11,7 @@ import { useInView } from 'react-intersection-observer'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; import useScheduleStore from '../../../stores/useScheduleStore'; +import useToast from '../../../hooks/useToast'; import { getTodayKST, formatDate } from '../../../utils/date'; import * as schedulesApi from '../../../api/admin/schedules'; import * as categoriesApi from '../../../api/admin/categories'; @@ -138,7 +139,7 @@ function AdminSchedule() { // 로컬 상태 (페이지 이동 시 유지할 필요 없는 것들) const [loading, setLoading] = useState(false); const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const scrollContainerRef = useRef(null); const SEARCH_LIMIT = 5; // 테스트용 5개 @@ -304,14 +305,6 @@ function AdminSchedule() { return cat?.color || '#4A7C59'; }; - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); - useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); diff --git a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx index 0d5049c..d730c86 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx @@ -7,26 +7,19 @@ import { } from 'lucide-react'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; +import useToast from '../../../hooks/useToast'; import * as botsApi from '../../../api/admin/bots'; function AdminScheduleBots() { const navigate = useNavigate(); const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const [bots, setBots] = useState([]); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용) const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태 - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - } - }, [toast]); - useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); diff --git a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx index 6d5cd24..2fc375d 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx @@ -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 { HexColorPicker } from 'react-colorful'; import Toast from '../../../components/Toast'; +import useToast from '../../../hooks/useToast'; import * as authApi from '../../../api/admin/auth'; import * as categoriesApi from '../../../api/admin/categories'; @@ -38,13 +39,7 @@ function AdminScheduleCategory() { const [user, setUser] = useState(null); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); - const [toast, setToast] = useState(null); - - // 토스트 표시 (3초 후 자동 닫힘) - const showToast = (type, message) => { - setToast({ type, message }); - setTimeout(() => setToast(null), 3000); - }; + const { toast, setToast, showSuccess, showError } = useToast(); // 모달 상태 const [modalOpen, setModalOpen] = useState(false); @@ -84,7 +79,7 @@ function AdminScheduleCategory() { setCategories(data); } catch (error) { console.error('카테고리 조회 오류:', error); - showToast('error', '카테고리를 불러오는데 실패했습니다.'); + showError('카테고리를 불러오는데 실패했습니다.'); } finally { setLoading(false); } @@ -112,7 +107,7 @@ function AdminScheduleCategory() { // 카테고리 저장 const handleSave = async () => { if (!formData.name.trim()) { - showToast('error', '카테고리 이름을 입력해주세요.'); + showError('카테고리 이름을 입력해주세요.'); return; } @@ -122,7 +117,7 @@ function AdminScheduleCategory() { && cat.id !== editingCategory?.id ); if (isDuplicate) { - showToast('error', '이미 존재하는 카테고리입니다.'); + showError('이미 존재하는 카테고리입니다.'); return; } @@ -132,12 +127,12 @@ function AdminScheduleCategory() { } else { await categoriesApi.createCategory(formData); } - showToast('success', editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.'); + showSuccess(editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.'); setModalOpen(false); fetchCategories(); } catch (error) { console.error('저장 오류:', error); - showToast('error', error.message || '저장에 실패했습니다.'); + showError(error.message || '저장에 실패했습니다.'); } }; @@ -153,13 +148,13 @@ function AdminScheduleCategory() { try { await categoriesApi.deleteCategory(deleteTarget.id); - showToast('success', '카테고리가 삭제되었습니다.'); + showSuccess('카테고리가 삭제되었습니다.'); setDeleteDialogOpen(false); setDeleteTarget(null); fetchCategories(); } catch (error) { console.error('삭제 오류:', error); - showToast('error', error.message || '삭제에 실패했습니다.'); + showError(error.message || '삭제에 실패했습니다.'); } }; diff --git a/frontend/src/pages/pc/admin/AdminScheduleForm.jsx b/frontend/src/pages/pc/admin/AdminScheduleForm.jsx index d3131a0..fdd1fdb 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleForm.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleForm.jsx @@ -27,6 +27,7 @@ import { import Toast from "../../../components/Toast"; import Lightbox from "../../../components/common/Lightbox"; import CustomDatePicker from "../../../components/admin/CustomDatePicker"; +import useToast from "../../../hooks/useToast"; import * as authApi from "../../../api/admin/auth"; import * as categoriesApi from "../../../api/admin/categories"; import * as schedulesApi from "../../../api/admin/schedules"; @@ -401,7 +402,7 @@ function AdminScheduleForm() { const isEditMode = !!id; const [user, setUser] = useState(null); - const [toast, setToast] = useState(null); + const { toast, setToast } = useToast(); const [loading, setLoading] = useState(false); 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(() => { if (!authApi.hasToken()) { navigate("/admin");