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:
caadiq 2026-01-09 22:15:10 +09:00
parent 0722ca10f3
commit fbb68e66ee
5 changed files with 115 additions and 141 deletions

View 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");
}

View file

@ -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 }),
});
}

View file

@ -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,26 +74,19 @@ function AdminDashboard() {
const fetchStats = async () => {
//
try {
const membersRes = await fetch('/api/members');
if (membersRes.ok) {
const members = await membersRes.json();
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();
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();
const detail = await getAlbum(album.id);
if (detail.conceptPhotos) {
Object.values(detail.conceptPhotos).forEach(photos => {
totalPhotos += photos.length;
@ -108,25 +95,20 @@ function AdminDashboard() {
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();
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');
};

View file

@ -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) {
if (!authApi.hasToken()) {
setCheckingAuth(false);
return;
}
//
fetch('/api/admin/verify', {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(res => res.json())
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);

View file

@ -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,16 +60,12 @@ 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())
authApi.verifyToken()
.then(data => {
if (data.valid) {
setUser(data.user);
@ -82,8 +80,7 @@ function AdminScheduleCategory() {
//
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) {
if (editingCategory) {
await categoriesApi.updateCategory(editingCategory.id, formData);
} else {
await categoriesApi.createCategory(formData);
}
showToast('success', editingCategory ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.');
setModalOpen(false);
fetchCategories();
} else {
const error = await res.json();
showToast('error', error.error || '저장에 실패했습니다.');
}
} 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) {
await categoriesApi.deleteCategory(deleteTarget.id);
showToast('success', '카테고리가 삭제되었습니다.');
setDeleteDialogOpen(false);
setDeleteTarget(null);
fetchCategories();
} else {
const error = await res.json();
showToast('error', error.error || '삭제에 실패했습니다.');
}
} 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(); //