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';
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: '',

View file

@ -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()) {

View file

@ -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()) {

View file

@ -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()) {

View file

@ -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(() => {
//

View file

@ -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');

View file

@ -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');

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 { 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 || '삭제에 실패했습니다.');
}
};

View file

@ -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");