refactor: 어드민 페이지 인증 로직을 useAdminAuth 훅으로 통합

- 8개 어드민 페이지에서 중복된 인증 코드 제거
- localStorage 직접 접근 → useAdminAuth 훅 사용
- authApi.hasToken()/verifyToken() 호출 제거
- 일관된 인증 상태 관리 (5분 캐시, 자동 리다이렉트)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-16 23:10:30 +09:00
parent d44537d870
commit 6462949bc7
8 changed files with 78 additions and 145 deletions

View file

@ -1,13 +1,14 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useNavigate, useParams, Link } from 'react-router-dom'; import { useNavigate, useParams, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
Save, Home, ChevronRight, Music, Trash2, Plus, Image, Star, Save, Home, ChevronRight, Music, Trash2, Plus, Image, Star,
ChevronDown ChevronDown
} 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 AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
// //
@ -79,14 +80,14 @@ function AdminAlbumForm() {
const { id } = useParams(); const { id } = useParams();
const isEditMode = !!id; const isEditMode = !!id;
const coverInputRef = useRef(null); const coverInputRef = useRef(null);
const { user, isAuthenticated } = useAdminAuth();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
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 } = useToast(); const { toast, setToast } = useToast();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '', title: '',
album_type: '', album_type: '',
@ -101,45 +102,36 @@ function AdminAlbumForm() {
const [tracks, setTracks] = useState([]); const [tracks, setTracks] = useState([]);
//
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('adminToken'); if (!isAuthenticated || !isEditMode) return;
const userData = localStorage.getItem('adminUser');
if (!token || !userData) { setLoading(true);
navigate('/admin'); fetch(`/api/albums/${id}`)
return; .then(res => res.json())
} .then(data => {
setFormData({
setUser(JSON.parse(userData)); title: data.title || '',
album_type: data.album_type || '',
if (isEditMode) { album_type_short: data.album_type_short || '',
setLoading(true); release_date: data.release_date ? data.release_date.split('T')[0] : '',
fetch(`/api/albums/${id}`) cover_original_url: data.cover_original_url || '',
.then(res => res.json()) cover_medium_url: data.cover_medium_url || '',
.then(data => { cover_thumb_url: data.cover_thumb_url || '',
setFormData({ folder_name: data.folder_name || '',
title: data.title || '', description: data.description || '',
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);
}); });
} if (data.cover_medium_url || data.cover_original_url) {
}, [id, isEditMode, navigate]); 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 handleInputChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;

View file

@ -10,8 +10,8 @@ import {
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
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';
import * as albumsApi from '../../../api/admin/albums'; import * as albumsApi from '../../../api/admin/albums';
@ -22,11 +22,11 @@ function AdminAlbumPhotos() {
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const photoListRef = useRef(null); // ref const photoListRef = useRef(null); // ref
const { user, isAuthenticated } = useAdminAuth();
const [album, setAlbum] = useState(null); const [album, setAlbum] = useState(null);
const [photos, setPhotos] = useState([]); const [photos, setPhotos] = useState([]);
const [teasers, setTeasers] = useState([]); // const [teasers, setTeasers] = useState([]); //
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
const { toast, setToast } = useToast(); const { toast, setToast } = useToast();
const [selectedPhotos, setSelectedPhotos] = useState([]); const [selectedPhotos, setSelectedPhotos] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -143,15 +143,10 @@ function AdminAlbumPhotos() {
}; };
useEffect(() => { useEffect(() => {
// if (isAuthenticated) {
if (!authApi.hasToken()) { fetchAlbumData();
navigate('/admin');
return;
} }
}, [isAuthenticated, albumId]);
setUser(authApi.getCurrentUser());
fetchAlbumData();
}, [navigate, albumId]);
const fetchAlbumData = async () => { const fetchAlbumData = async () => {
try { try {

View file

@ -1,40 +1,32 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion } from 'framer-motion';
import { import { Plus, Search, Edit2, Trash2, Image, Music, Home, ChevronRight, Calendar } from 'lucide-react';
Plus, Search, Edit2, Trash2, Image, Music,
Home, ChevronRight, Calendar, X
} from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
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';
function AdminAlbums() { function AdminAlbums() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, isAuthenticated } = useAdminAuth();
const { toast, setToast } = useToast();
const [albums, setAlbums] = useState([]); const [albums, setAlbums] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [user, setUser] = 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);
useEffect(() => { useEffect(() => {
// if (isAuthenticated) {
if (!authApi.hasToken()) { fetchAlbums();
navigate('/admin');
return;
} }
}, [isAuthenticated]);
setUser(authApi.getCurrentUser());
fetchAlbums();
}, [navigate]);
const fetchAlbums = async () => { const fetchAlbums = async () => {
try { try {

View file

@ -1,12 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { import { Disc3, Calendar, Users, Home, ChevronRight } from 'lucide-react';
Disc3, Calendar, Users,
Home, ChevronRight
} from 'lucide-react';
import AdminLayout from '../../../components/admin/AdminLayout'; 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 { getMembers } from '../../../api/public/members';
import { getAlbums, getAlbum } from '../../../api/public/albums'; import { getAlbums, getAlbum } from '../../../api/public/albums';
import { getSchedules } from '../../../api/public/schedules'; import { getSchedules } from '../../../api/public/schedules';
@ -43,8 +40,7 @@ function AnimatedNumber({ value }) {
} }
function AdminDashboard() { function AdminDashboard() {
const navigate = useNavigate(); const { user, isAuthenticated } = useAdminAuth();
const [user, setUser] = useState(null);
const [stats, setStats] = useState({ const [stats, setStats] = useState({
albums: 0, albums: 0,
photos: 0, photos: 0,
@ -53,24 +49,10 @@ function AdminDashboard() {
}); });
useEffect(() => { useEffect(() => {
// if (isAuthenticated) {
if (!authApi.hasToken()) { fetchStats();
navigate('/admin');
return;
} }
}, [isAuthenticated]);
setUser(authApi.getCurrentUser());
//
authApi.verifyToken()
.catch(() => {
authApi.logout();
navigate('/admin');
});
//
fetchStats();
}, [navigate]);
const fetchStats = async () => { const fetchStats = async () => {
// //

View file

@ -14,6 +14,7 @@ import Tooltip from '../../../components/Tooltip';
import AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useScheduleStore from '../../../stores/useScheduleStore'; import useScheduleStore from '../../../stores/useScheduleStore';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; 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';
@ -151,9 +152,10 @@ function AdminSchedule() {
scrollPosition, setScrollPosition, scrollPosition, setScrollPosition,
} = useScheduleStore(); } = useScheduleStore();
const { user, isAuthenticated } = useAdminAuth();
// ( ) // ( )
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [user, setUser] = useState(null);
const { toast, setToast } = useToast(); const { toast, setToast } = useToast();
const scrollContainerRef = useRef(null); const scrollContainerRef = useRef(null);
const searchContainerRef = useRef(null); // ( ) const searchContainerRef = useRef(null); // ( )
@ -363,26 +365,18 @@ function AdminSchedule() {
}; };
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('adminToken'); if (!isAuthenticated) return;
const userData = localStorage.getItem('adminUser');
if (!token || !userData) {
navigate('/admin');
return;
}
setUser(JSON.parse(userData));
// //
fetchCategories(); fetchCategories();
// sessionStorage ( / ) // sessionStorage ( / )
const savedToast = sessionStorage.getItem('scheduleToast'); const savedToast = sessionStorage.getItem('scheduleToast');
if (savedToast) { if (savedToast) {
setToast(JSON.parse(savedToast)); setToast(JSON.parse(savedToast));
sessionStorage.removeItem('scheduleToast'); sessionStorage.removeItem('scheduleToast');
} }
}, [navigate]); }, [isAuthenticated]);
// //

View file

@ -8,6 +8,7 @@ import {
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as botsApi from '../../../api/admin/bots'; import * as botsApi from '../../../api/admin/bots';
@ -43,7 +44,7 @@ const MeilisearchIcon = ({ size = 20 }) => (
function AdminScheduleBots() { function AdminScheduleBots() {
const navigate = useNavigate(); const navigate = useNavigate();
const [user, setUser] = useState(null); const { user, isAuthenticated } = useAdminAuth();
const { toast, setToast } = useToast(); const { toast, setToast } = useToast();
const [bots, setBots] = useState([]); const [bots, setBots] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -52,18 +53,11 @@ function AdminScheduleBots() {
const [quotaWarning, setQuotaWarning] = useState(null); // const [quotaWarning, setQuotaWarning] = useState(null); //
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('adminToken'); if (isAuthenticated) {
const userData = localStorage.getItem('adminUser'); fetchBots();
fetchQuotaWarning();
if (!token || !userData) {
navigate('/admin');
return;
} }
}, [isAuthenticated]);
setUser(JSON.parse(userData));
fetchBots();
fetchQuotaWarning();
}, [navigate]);
// //
const fetchBots = async () => { const fetchBots = async () => {

View file

@ -6,8 +6,8 @@ import { HexColorPicker } from 'react-colorful';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import AdminLayout from '../../../components/admin/AdminLayout'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth';
import * as categoriesApi from '../../../api/admin/categories'; import * as categoriesApi from '../../../api/admin/categories';
// (8) // (8)
@ -38,41 +38,29 @@ const getColorStyle = (colorValue) => {
function AdminScheduleCategory() { function AdminScheduleCategory() {
const navigate = useNavigate(); const navigate = useNavigate();
const [user, setUser] = useState(null); const { user, isAuthenticated } = useAdminAuth();
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { toast, setToast, showSuccess, showError } = useToast(); const { toast, setToast, showSuccess, showError } = useToast();
// //
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState(null); const [editingCategory, setEditingCategory] = useState(null);
const [formData, setFormData] = useState({ name: '', color: 'blue' }); const [formData, setFormData] = useState({ name: '', color: 'blue' });
// //
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
// //
const [colorPickerOpen, setColorPickerOpen] = useState(false); const [colorPickerOpen, setColorPickerOpen] = useState(false);
// //
useEffect(() => { useEffect(() => {
if (!authApi.hasToken()) { if (isAuthenticated) {
navigate('/admin'); fetchCategories();
return;
} }
}, [isAuthenticated]);
authApi.verifyToken()
.then(data => {
if (data.valid) {
setUser(data.user);
fetchCategories();
} else {
navigate('/admin');
}
})
.catch(() => navigate('/admin'));
}, [navigate]);
// //
const fetchCategories = async () => { const fetchCategories = async () => {

View file

@ -28,8 +28,8 @@ import CustomDatePicker from "../../../components/admin/CustomDatePicker";
import CustomTimePicker from "../../../components/admin/CustomTimePicker"; import CustomTimePicker from "../../../components/admin/CustomTimePicker";
import AdminLayout from "../../../components/admin/AdminLayout"; import AdminLayout from "../../../components/admin/AdminLayout";
import ConfirmDialog from "../../../components/admin/ConfirmDialog"; import ConfirmDialog from "../../../components/admin/ConfirmDialog";
import useAdminAuth from "../../../hooks/useAdminAuth";
import useToast from "../../../hooks/useToast"; import useToast from "../../../hooks/useToast";
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";
import { getMembers } from "../../../api/public/members"; import { getMembers } from "../../../api/public/members";
@ -38,8 +38,8 @@ function AdminScheduleForm() {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const isEditMode = !!id; const isEditMode = !!id;
const { user, isAuthenticated } = useAdminAuth();
const [user, setUser] = useState(null);
const { toast, setToast } = useToast(); const { toast, setToast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
@ -147,12 +147,8 @@ function AdminScheduleForm() {
}; };
useEffect(() => { useEffect(() => {
if (!authApi.hasToken()) { if (!isAuthenticated) return;
navigate("/admin");
return;
}
setUser(authApi.getCurrentUser());
fetchMembers(); fetchMembers();
fetchCategories(); fetchCategories();
@ -160,7 +156,7 @@ function AdminScheduleForm() {
if (isEditMode && id) { if (isEditMode && id) {
fetchSchedule(); fetchSchedule();
} }
}, [navigate, isEditMode, id]); }, [isAuthenticated, isEditMode, id]);
// ( ) // ( )
const fetchSchedule = async () => { const fetchSchedule = async () => {