From 6462949bc7d6ec390afbe8c9ef252a0e50a2cd7e Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 16 Jan 2026 23:10:30 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B8=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20useAdminAuth=20=ED=9B=85=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 8개 어드민 페이지에서 중복된 인증 코드 제거 - localStorage 직접 접근 → useAdminAuth 훅 사용 - authApi.hasToken()/verifyToken() 호출 제거 - 일관된 인증 상태 관리 (5분 캐시, 자동 리다이렉트) Co-Authored-By: Claude Opus 4.5 --- .../src/pages/pc/admin/AdminAlbumForm.jsx | 72 +++++++++---------- .../src/pages/pc/admin/AdminAlbumPhotos.jsx | 15 ++-- frontend/src/pages/pc/admin/AdminAlbums.jsx | 26 +++---- .../src/pages/pc/admin/AdminDashboard.jsx | 32 ++------- frontend/src/pages/pc/admin/AdminSchedule.jsx | 18 ++--- .../src/pages/pc/admin/AdminScheduleBots.jsx | 18 ++--- .../pages/pc/admin/AdminScheduleCategory.jsx | 30 +++----- .../src/pages/pc/admin/AdminScheduleForm.jsx | 12 ++-- 8 files changed, 78 insertions(+), 145 deletions(-) diff --git a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx index 38d9821..6494219 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx @@ -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; diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx index 790553b..c61e1bf 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx @@ -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 { diff --git a/frontend/src/pages/pc/admin/AdminAlbums.jsx b/frontend/src/pages/pc/admin/AdminAlbums.jsx index 0b0803d..d9b3caa 100644 --- a/frontend/src/pages/pc/admin/AdminAlbums.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbums.jsx @@ -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 { diff --git a/frontend/src/pages/pc/admin/AdminDashboard.jsx b/frontend/src/pages/pc/admin/AdminDashboard.jsx index 2bab25b..eb28c70 100644 --- a/frontend/src/pages/pc/admin/AdminDashboard.jsx +++ b/frontend/src/pages/pc/admin/AdminDashboard.jsx @@ -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 () => { // 각 통계를 개별적으로 가져와서 하나가 실패해도 다른 것은 표시 diff --git a/frontend/src/pages/pc/admin/AdminSchedule.jsx b/frontend/src/pages/pc/admin/AdminSchedule.jsx index 91082fc..ff2c9f9 100644 --- a/frontend/src/pages/pc/admin/AdminSchedule.jsx +++ b/frontend/src/pages/pc/admin/AdminSchedule.jsx @@ -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]); // 월이 변경될 때마다 일정 로드 diff --git a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx index b3c4b63..2f8b746 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx @@ -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 () => { diff --git a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx index 8f97904..78eccbe 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx @@ -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 () => { diff --git a/frontend/src/pages/pc/admin/AdminScheduleForm.jsx b/frontend/src/pages/pc/admin/AdminScheduleForm.jsx index e747dea..649c52b 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleForm.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleForm.jsx @@ -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 () => {