diff --git a/frontend/src/api/admin/auth.js b/frontend/src/api/admin/auth.js new file mode 100644 index 0000000..a162fe7 --- /dev/null +++ b/frontend/src/api/admin/auth.js @@ -0,0 +1,42 @@ +/** + * 어드민 인증 API + */ +import { fetchAdminApi } from "../index"; + +// 토큰 검증 +export async function verifyToken() { + return fetchAdminApi("/api/admin/verify"); +} + +// 로그인 +export async function login(username, password) { + const response = await fetch("/api/admin/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "로그인 실패"); + } + + return response.json(); +} + +// 로그아웃 (로컬 스토리지 정리) +export function logout() { + localStorage.removeItem("adminToken"); + localStorage.removeItem("adminUser"); +} + +// 현재 사용자 정보 가져오기 +export function getCurrentUser() { + const userData = localStorage.getItem("adminUser"); + return userData ? JSON.parse(userData) : null; +} + +// 토큰 존재 여부 확인 +export function hasToken() { + return !!localStorage.getItem("adminToken"); +} diff --git a/frontend/src/api/admin/categories.js b/frontend/src/api/admin/categories.js index c584830..05992b0 100644 --- a/frontend/src/api/admin/categories.js +++ b/frontend/src/api/admin/categories.js @@ -32,9 +32,9 @@ export async function deleteCategory(id) { } // 카테고리 순서 변경 -export async function reorderCategories(orderedIds) { - return fetchAdminApi("/api/admin/schedule-categories/reorder", { +export async function reorderCategories(orders) { + return fetchAdminApi("/api/admin/schedule-categories-order", { method: "PUT", - body: JSON.stringify({ orderedIds }), + body: JSON.stringify({ orders }), }); } diff --git a/frontend/src/pages/pc/admin/AdminDashboard.jsx b/frontend/src/pages/pc/admin/AdminDashboard.jsx index e2d22f7..9b9af26 100644 --- a/frontend/src/pages/pc/admin/AdminDashboard.jsx +++ b/frontend/src/pages/pc/admin/AdminDashboard.jsx @@ -5,6 +5,10 @@ import { Disc3, Calendar, Users, LogOut, Home, ChevronRight } from 'lucide-react'; +import * as authApi from '../../../api/admin/auth'; +import { getMembers } from '../../../api/public/members'; +import { getAlbums, getAlbum } from '../../../api/public/albums'; +import { getSchedules } from '../../../api/public/schedules'; // 슬롯머신 스타일 롤링 숫자 컴포넌트 (아래에서 위로) function AnimatedNumber({ value }) { @@ -49,27 +53,17 @@ function AdminDashboard() { useEffect(() => { // 로그인 상태 확인 - const token = localStorage.getItem('adminToken'); - const userData = localStorage.getItem('adminUser'); - - if (!token || !userData) { + if (!authApi.hasToken()) { navigate('/admin'); return; } - setUser(JSON.parse(userData)); + setUser(authApi.getCurrentUser()); // 토큰 유효성 검증 - fetch('/api/admin/verify', { - headers: { Authorization: `Bearer ${token}` } - }) - .then(res => { - if (!res.ok) throw new Error('Invalid token'); - return res.json(); - }) + authApi.verifyToken() .catch(() => { - localStorage.removeItem('adminToken'); - localStorage.removeItem('adminUser'); + authApi.logout(); navigate('/admin'); }); @@ -80,53 +74,41 @@ function AdminDashboard() { const fetchStats = async () => { // 각 통계를 개별적으로 가져와서 하나가 실패해도 다른 것은 표시 try { - const membersRes = await fetch('/api/members'); - if (membersRes.ok) { - const members = await membersRes.json(); - setStats(prev => ({ ...prev, members: members.filter(m => !m.is_former).length })); - } + const members = await getMembers(); + setStats(prev => ({ ...prev, members: members.filter(m => !m.is_former).length })); } catch (e) { console.error('멤버 통계 오류:', e); } try { - const albumsRes = await fetch('/api/albums'); - if (albumsRes.ok) { - const albums = await albumsRes.json(); - setStats(prev => ({ ...prev, albums: albums.length })); - - // 사진 수 계산 - let totalPhotos = 0; - for (const album of albums) { - try { - const detailRes = await fetch(`/api/albums/${album.id}`); - if (detailRes.ok) { - const detail = await detailRes.json(); - if (detail.conceptPhotos) { - Object.values(detail.conceptPhotos).forEach(photos => { - totalPhotos += photos.length; - }); - } - if (detail.teasers) { - totalPhotos += detail.teasers.length; - } - } - } catch (e) { /* 개별 앨범 오류 무시 */ } - } - setStats(prev => ({ ...prev, photos: totalPhotos })); + const albums = await getAlbums(); + setStats(prev => ({ ...prev, albums: albums.length })); + + // 사진 수 계산 + let totalPhotos = 0; + for (const album of albums) { + try { + const detail = await getAlbum(album.id); + if (detail.conceptPhotos) { + Object.values(detail.conceptPhotos).forEach(photos => { + totalPhotos += photos.length; + }); + } + if (detail.teasers) { + totalPhotos += detail.teasers.length; + } + } catch (e) { /* 개별 앨범 오류 무시 */ } } + setStats(prev => ({ ...prev, photos: totalPhotos })); } catch (e) { console.error('앨범 통계 오류:', e); } try { - const schedulesRes = await fetch('/api/schedules'); - if (schedulesRes.ok) { - const schedules = await schedulesRes.json(); - setStats(prev => ({ ...prev, schedules: Array.isArray(schedules) ? schedules.length : 0 })); - } + const today = new Date(); + const schedules = await getSchedules(today.getFullYear(), today.getMonth() + 1); + setStats(prev => ({ ...prev, schedules: Array.isArray(schedules) ? schedules.length : 0 })); } catch (e) { console.error('일정 통계 오류:', e); } }; const handleLogout = () => { - localStorage.removeItem('adminToken'); - localStorage.removeItem('adminUser'); + authApi.logout(); navigate('/admin'); }; diff --git a/frontend/src/pages/pc/admin/AdminLogin.jsx b/frontend/src/pages/pc/admin/AdminLogin.jsx index f326cad..fbc0679 100644 --- a/frontend/src/pages/pc/admin/AdminLogin.jsx +++ b/frontend/src/pages/pc/admin/AdminLogin.jsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react'; +import * as authApi from '../../../api/admin/auth'; function AdminLogin() { const navigate = useNavigate(); @@ -14,13 +15,13 @@ function AdminLogin() { // 이미 로그인되어 있으면 대시보드로 리다이렉트 useEffect(() => { - const token = localStorage.getItem('adminToken'); - if (token) { - // 토큰 유효성 검증 - fetch('/api/admin/verify', { - headers: { 'Authorization': `Bearer ${token}` } - }) - .then(res => res.json()) + if (!authApi.hasToken()) { + setCheckingAuth(false); + return; + } + + // 토큰 유효성 검증 + authApi.verifyToken() .then(data => { if (data.valid) { navigate('/admin/dashboard'); @@ -29,9 +30,6 @@ function AdminLogin() { } }) .catch(() => setCheckingAuth(false)); - } else { - setCheckingAuth(false); - } }, [navigate]); const handleSubmit = async (e) => { @@ -40,17 +38,7 @@ function AdminLogin() { setLoading(true); try { - const response = await fetch('/api/admin/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || '로그인에 실패했습니다.'); - } + const data = await authApi.login(username, password); // JWT 토큰 저장 localStorage.setItem('adminToken', data.token); diff --git a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx index 1d25953..6d5cd24 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleCategory.jsx @@ -4,6 +4,8 @@ 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 * as authApi from '../../../api/admin/auth'; +import * as categoriesApi from '../../../api/admin/categories'; // 기본 색상 (8개) const colorOptions = [ @@ -58,32 +60,27 @@ function AdminScheduleCategory() { // 사용자 인증 확인 useEffect(() => { - const token = localStorage.getItem('adminToken'); - if (!token) { + if (!authApi.hasToken()) { navigate('/admin'); return; } - fetch('/api/admin/verify', { - headers: { 'Authorization': `Bearer ${token}` } - }) - .then(res => res.json()) - .then(data => { - if (data.valid) { - setUser(data.user); - fetchCategories(); - } else { - navigate('/admin'); - } - }) - .catch(() => navigate('/admin')); + authApi.verifyToken() + .then(data => { + if (data.valid) { + setUser(data.user); + fetchCategories(); + } else { + navigate('/admin'); + } + }) + .catch(() => navigate('/admin')); }, [navigate]); // 카테고리 목록 조회 const fetchCategories = async () => { try { - const res = await fetch('/api/admin/schedule-categories'); - const data = await res.json(); + const data = await categoriesApi.getCategories(); setCategories(data); } catch (error) { console.error('카테고리 조회 오류:', error); @@ -95,8 +92,7 @@ function AdminScheduleCategory() { // 로그아웃 const handleLogout = () => { - localStorage.removeItem('adminToken'); - localStorage.removeItem('adminUser'); + authApi.logout(); navigate('/admin'); }; @@ -129,34 +125,19 @@ function AdminScheduleCategory() { showToast('error', '이미 존재하는 카테고리입니다.'); return; } - - const token = localStorage.getItem('adminToken'); try { - const url = editingCategory - ? `/api/admin/schedule-categories/${editingCategory.id}` - : '/api/admin/schedule-categories'; - - const res = await fetch(url, { - method: editingCategory ? 'PUT' : 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify(formData) - }); - - if (res.ok) { - showToast('success', editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.'); - setModalOpen(false); - fetchCategories(); + if (editingCategory) { + await categoriesApi.updateCategory(editingCategory.id, formData); } else { - const error = await res.json(); - showToast('error', error.error || '저장에 실패했습니다.'); + await categoriesApi.createCategory(formData); } + showToast('success', editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.'); + setModalOpen(false); + fetchCategories(); } catch (error) { console.error('저장 오류:', error); - showToast('error', '저장 중 오류가 발생했습니다.'); + showToast('error', error.message || '저장에 실패했습니다.'); } }; @@ -170,26 +151,15 @@ function AdminScheduleCategory() { const handleDelete = async () => { if (!deleteTarget) return; - const token = localStorage.getItem('adminToken'); - try { - const res = await fetch(`/api/admin/schedule-categories/${deleteTarget.id}`, { - method: 'DELETE', - headers: { 'Authorization': `Bearer ${token}` } - }); - - if (res.ok) { - showToast('success', '카테고리가 삭제되었습니다.'); - setDeleteDialogOpen(false); - setDeleteTarget(null); - fetchCategories(); - } else { - const error = await res.json(); - showToast('error', error.error || '삭제에 실패했습니다.'); - } + await categoriesApi.deleteCategory(deleteTarget.id); + showToast('success', '카테고리가 삭제되었습니다.'); + setDeleteDialogOpen(false); + setDeleteTarget(null); + fetchCategories(); } catch (error) { console.error('삭제 오류:', error); - showToast('error', '삭제 중 오류가 발생했습니다.'); + showToast('error', error.message || '삭제에 실패했습니다.'); } }; @@ -203,16 +173,8 @@ function AdminScheduleCategory() { sort_order: idx + 1 })); - const token = localStorage.getItem('adminToken'); try { - await fetch('/api/admin/schedule-categories-order', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ orders }) - }); + await categoriesApi.reorderCategories(orders); } catch (error) { console.error('순서 업데이트 오류:', error); fetchCategories(); // 실패시 원래 데이터 다시 불러오기