refactor: 인증 API 모듈화 및 Admin 페이지 적용 완료
- api/admin/auth.js 인증 모듈 생성 (verifyToken, login, logout, hasToken 등) - categories.js reorderCategories 경로 수정 - AdminScheduleCategory 전체 API 모듈 적용 - AdminLogin API 모듈 적용 - AdminDashboard API 모듈 적용 남은 작업: AdminAlbums, AdminAlbumPhotos, AdminMemberEdit, AdminScheduleForm
This commit is contained in:
parent
0722ca10f3
commit
fbb68e66ee
5 changed files with 115 additions and 141 deletions
42
frontend/src/api/admin/auth.js
Normal file
42
frontend/src/api/admin/auth.js
Normal file
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
const albums = await getAlbums();
|
||||
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 }));
|
||||
// 사진 수 계산
|
||||
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');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
|
||||
|
|
@ -130,33 +126,18 @@ function AdminScheduleCategory() {
|
|||
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(); // 실패시 원래 데이터 다시 불러오기
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue