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:
parent
3367f4806d
commit
0b00055773
10 changed files with 81 additions and 86 deletions
56
frontend/src/hooks/useToast.js
Normal file
56
frontend/src/hooks/useToast.js
Normal 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;
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
// 로그인 확인
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 || '삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue