refactor: 어드민 페이지 인증 로직을 useAdminAuth 훅으로 통합
- 8개 어드민 페이지에서 중복된 인증 코드 제거 - localStorage 직접 접근 → useAdminAuth 훅 사용 - authApi.hasToken()/verifyToken() 호출 제거 - 일관된 인증 상태 관리 (5분 캐시, 자동 리다이렉트) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d44537d870
commit
6462949bc7
8 changed files with 78 additions and 145 deletions
|
|
@ -1,13 +1,14 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
import {
|
||||
Save, Home, ChevronRight, Music, Trash2, Plus, Image, Star,
|
||||
ChevronDown
|
||||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import CustomDatePicker from '../../../components/admin/CustomDatePicker';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
// 커스텀 드롭다운 컴포넌트
|
||||
|
|
@ -79,14 +80,14 @@ function AdminAlbumForm() {
|
|||
const { id } = useParams();
|
||||
const isEditMode = !!id;
|
||||
const coverInputRef = useRef(null);
|
||||
|
||||
const [user, setUser] = useState(null);
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [coverPreview, setCoverPreview] = useState(null);
|
||||
const [coverFile, setCoverFile] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
album_type: '',
|
||||
|
|
@ -101,45 +102,36 @@ function AdminAlbumForm() {
|
|||
|
||||
const [tracks, setTracks] = useState([]);
|
||||
|
||||
// 수정 모드일 때 앨범 데이터 로드
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userData = localStorage.getItem('adminUser');
|
||||
if (!isAuthenticated || !isEditMode) return;
|
||||
|
||||
if (!token || !userData) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(JSON.parse(userData));
|
||||
|
||||
if (isEditMode) {
|
||||
setLoading(true);
|
||||
fetch(`/api/albums/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setFormData({
|
||||
title: data.title || '',
|
||||
album_type: data.album_type || '',
|
||||
album_type_short: data.album_type_short || '',
|
||||
release_date: data.release_date ? data.release_date.split('T')[0] : '',
|
||||
cover_original_url: data.cover_original_url || '',
|
||||
cover_medium_url: data.cover_medium_url || '',
|
||||
cover_thumb_url: data.cover_thumb_url || '',
|
||||
folder_name: data.folder_name || '',
|
||||
description: data.description || '',
|
||||
});
|
||||
if (data.cover_medium_url || data.cover_original_url) {
|
||||
setCoverPreview(data.cover_medium_url || data.cover_original_url);
|
||||
}
|
||||
setTracks(data.tracks || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('앨범 로드 오류:', error);
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
fetch(`/api/albums/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setFormData({
|
||||
title: data.title || '',
|
||||
album_type: data.album_type || '',
|
||||
album_type_short: data.album_type_short || '',
|
||||
release_date: data.release_date ? data.release_date.split('T')[0] : '',
|
||||
cover_original_url: data.cover_original_url || '',
|
||||
cover_medium_url: data.cover_medium_url || '',
|
||||
cover_thumb_url: data.cover_thumb_url || '',
|
||||
folder_name: data.folder_name || '',
|
||||
description: data.description || '',
|
||||
});
|
||||
}
|
||||
}, [id, isEditMode, navigate]);
|
||||
if (data.cover_medium_url || data.cover_original_url) {
|
||||
setCoverPreview(data.cover_medium_url || data.cover_original_url);
|
||||
}
|
||||
setTracks(data.tracks || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('앨범 로드 오류:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id, isEditMode, isAuthenticated]);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import {
|
|||
import Toast from '../../../components/Toast';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import { getAlbum } from '../../../api/public/albums';
|
||||
import { getMembers } from '../../../api/public/members';
|
||||
import * as albumsApi from '../../../api/admin/albums';
|
||||
|
|
@ -22,11 +22,11 @@ function AdminAlbumPhotos() {
|
|||
const fileInputRef = useRef(null);
|
||||
const photoListRef = useRef(null); // 사진 목록 영역 ref
|
||||
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const [album, setAlbum] = useState(null);
|
||||
const [photos, setPhotos] = useState([]);
|
||||
const [teasers, setTeasers] = useState([]); // 티저 이미지
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
const [selectedPhotos, setSelectedPhotos] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
|
@ -143,15 +143,10 @@ function AdminAlbumPhotos() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 확인
|
||||
if (!authApi.hasToken()) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
if (isAuthenticated) {
|
||||
fetchAlbumData();
|
||||
}
|
||||
|
||||
setUser(authApi.getCurrentUser());
|
||||
fetchAlbumData();
|
||||
}, [navigate, albumId]);
|
||||
}, [isAuthenticated, albumId]);
|
||||
|
||||
const fetchAlbumData = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,32 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Plus, Search, Edit2, Trash2, Image, Music,
|
||||
Home, ChevronRight, Calendar, X
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Plus, Search, Edit2, Trash2, Image, Music, Home, ChevronRight, Calendar } from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
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';
|
||||
|
||||
|
||||
function AdminAlbums() {
|
||||
const navigate = useNavigate();
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const { toast, setToast } = useToast();
|
||||
|
||||
const [albums, setAlbums] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [user, setUser] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
const [deleteDialog, setDeleteDialog] = useState({ show: false, album: null });
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 확인
|
||||
if (!authApi.hasToken()) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
if (isAuthenticated) {
|
||||
fetchAlbums();
|
||||
}
|
||||
|
||||
setUser(authApi.getCurrentUser());
|
||||
fetchAlbums();
|
||||
}, [navigate]);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const fetchAlbums = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Disc3, Calendar, Users,
|
||||
Home, ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { Disc3, Calendar, Users, Home, ChevronRight } from 'lucide-react';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import { getMembers } from '../../../api/public/members';
|
||||
import { getAlbums, getAlbum } from '../../../api/public/albums';
|
||||
import { getSchedules } from '../../../api/public/schedules';
|
||||
|
|
@ -43,8 +40,7 @@ function AnimatedNumber({ value }) {
|
|||
}
|
||||
|
||||
function AdminDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState(null);
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const [stats, setStats] = useState({
|
||||
albums: 0,
|
||||
photos: 0,
|
||||
|
|
@ -53,24 +49,10 @@ function AdminDashboard() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 상태 확인
|
||||
if (!authApi.hasToken()) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
if (isAuthenticated) {
|
||||
fetchStats();
|
||||
}
|
||||
|
||||
setUser(authApi.getCurrentUser());
|
||||
|
||||
// 토큰 유효성 검증
|
||||
authApi.verifyToken()
|
||||
.catch(() => {
|
||||
authApi.logout();
|
||||
navigate('/admin');
|
||||
});
|
||||
|
||||
// 통계 데이터 가져오기
|
||||
fetchStats();
|
||||
}, [navigate]);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
// 각 통계를 개별적으로 가져와서 하나가 실패해도 다른 것은 표시
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import Tooltip from '../../../components/Tooltip';
|
|||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useScheduleStore from '../../../stores/useScheduleStore';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import { getTodayKST, formatDate } from '../../../utils/date';
|
||||
import * as schedulesApi from '../../../api/admin/schedules';
|
||||
|
|
@ -151,9 +152,10 @@ function AdminSchedule() {
|
|||
scrollPosition, setScrollPosition,
|
||||
} = useScheduleStore();
|
||||
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
|
||||
// 로컬 상태 (페이지 이동 시 유지할 필요 없는 것들)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
const scrollContainerRef = useRef(null);
|
||||
const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용)
|
||||
|
|
@ -363,26 +365,18 @@ function AdminSchedule() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userData = localStorage.getItem('adminUser');
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
if (!token || !userData) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(JSON.parse(userData));
|
||||
|
||||
// 카테고리 로드
|
||||
fetchCategories();
|
||||
|
||||
|
||||
// sessionStorage에서 토스트 메시지 확인 (일정 추가/수정 완료 시)
|
||||
const savedToast = sessionStorage.getItem('scheduleToast');
|
||||
if (savedToast) {
|
||||
setToast(JSON.parse(savedToast));
|
||||
sessionStorage.removeItem('scheduleToast');
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
|
||||
// 월이 변경될 때마다 일정 로드
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import Toast from '../../../components/Toast';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as botsApi from '../../../api/admin/bots';
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ const MeilisearchIcon = ({ size = 20 }) => (
|
|||
|
||||
function AdminScheduleBots() {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState(null);
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const { toast, setToast } = useToast();
|
||||
const [bots, setBots] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -52,18 +53,11 @@ function AdminScheduleBots() {
|
|||
const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const userData = localStorage.getItem('adminUser');
|
||||
|
||||
if (!token || !userData) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
if (isAuthenticated) {
|
||||
fetchBots();
|
||||
fetchQuotaWarning();
|
||||
}
|
||||
|
||||
setUser(JSON.parse(userData));
|
||||
fetchBots();
|
||||
fetchQuotaWarning();
|
||||
}, [navigate]);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// 봇 목록 조회
|
||||
const fetchBots = async () => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { HexColorPicker } from 'react-colorful';
|
|||
import Toast from '../../../components/Toast';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import * as categoriesApi from '../../../api/admin/categories';
|
||||
|
||||
// 기본 색상 (8개)
|
||||
|
|
@ -38,41 +38,29 @@ const getColorStyle = (colorValue) => {
|
|||
|
||||
function AdminScheduleCategory() {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState(null);
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast, setToast, showSuccess, showError } = useToast();
|
||||
|
||||
|
||||
// 모달 상태
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState(null);
|
||||
const [formData, setFormData] = useState({ name: '', color: 'blue' });
|
||||
|
||||
|
||||
// 삭제 다이얼로그
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
|
||||
|
||||
// 카스텀 컴러 피커 팝업
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
|
||||
// 사용자 인증 확인
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
if (!authApi.hasToken()) {
|
||||
navigate('/admin');
|
||||
return;
|
||||
if (isAuthenticated) {
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
authApi.verifyToken()
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
setUser(data.user);
|
||||
fetchCategories();
|
||||
} else {
|
||||
navigate('/admin');
|
||||
}
|
||||
})
|
||||
.catch(() => navigate('/admin'));
|
||||
}, [navigate]);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// 카테고리 목록 조회
|
||||
const fetchCategories = async () => {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import CustomDatePicker from "../../../components/admin/CustomDatePicker";
|
|||
import CustomTimePicker from "../../../components/admin/CustomTimePicker";
|
||||
import AdminLayout from "../../../components/admin/AdminLayout";
|
||||
import ConfirmDialog from "../../../components/admin/ConfirmDialog";
|
||||
import useAdminAuth from "../../../hooks/useAdminAuth";
|
||||
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";
|
||||
import { getMembers } from "../../../api/public/members";
|
||||
|
|
@ -38,8 +38,8 @@ function AdminScheduleForm() {
|
|||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const isEditMode = !!id;
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
|
||||
const [user, setUser] = useState(null);
|
||||
const { toast, setToast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [members, setMembers] = useState([]);
|
||||
|
|
@ -147,12 +147,8 @@ function AdminScheduleForm() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!authApi.hasToken()) {
|
||||
navigate("/admin");
|
||||
return;
|
||||
}
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
setUser(authApi.getCurrentUser());
|
||||
fetchMembers();
|
||||
fetchCategories();
|
||||
|
||||
|
|
@ -160,7 +156,7 @@ function AdminScheduleForm() {
|
|||
if (isEditMode && id) {
|
||||
fetchSchedule();
|
||||
}
|
||||
}, [navigate, isEditMode, id]);
|
||||
}, [isAuthenticated, isEditMode, id]);
|
||||
|
||||
// 기존 일정 데이터 로드 (수정 모드)
|
||||
const fetchSchedule = async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue