refactor: API 중앙화 시작 (1/3)
- api/ 디렉토리 구조 생성 - index.js: 공통 fetch 래퍼 - schedules.js, albums.js, members.js: 공개 API - admin/: 어드민 API (bots, albums, categories, members, schedules) - Schedule.jsx: API 모듈 적용 - AdminScheduleBots.jsx: API 모듈 적용
This commit is contained in:
parent
12e95003ae
commit
9886048a4c
11 changed files with 341 additions and 79 deletions
50
frontend/src/api/admin/albums.js
Normal file
50
frontend/src/api/admin/albums.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* 어드민 앨범 관리 API
|
||||
*/
|
||||
import { fetchAdminApi, fetchAdminFormData } from "../index";
|
||||
|
||||
// 앨범 목록 조회
|
||||
export async function getAlbums() {
|
||||
return fetchAdminApi("/api/admin/albums");
|
||||
}
|
||||
|
||||
// 앨범 상세 조회
|
||||
export async function getAlbum(id) {
|
||||
return fetchAdminApi(`/api/admin/albums/${id}`);
|
||||
}
|
||||
|
||||
// 앨범 생성
|
||||
export async function createAlbum(formData) {
|
||||
return fetchAdminFormData("/api/admin/albums", formData, "POST");
|
||||
}
|
||||
|
||||
// 앨범 수정
|
||||
export async function updateAlbum(id, formData) {
|
||||
return fetchAdminFormData(`/api/admin/albums/${id}`, formData, "PUT");
|
||||
}
|
||||
|
||||
// 앨범 삭제
|
||||
export async function deleteAlbum(id) {
|
||||
return fetchAdminApi(`/api/admin/albums/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// 앨범 사진 목록 조회
|
||||
export async function getAlbumPhotos(albumId) {
|
||||
return fetchAdminApi(`/api/admin/albums/${albumId}/photos`);
|
||||
}
|
||||
|
||||
// 앨범 사진 업로드
|
||||
export async function uploadAlbumPhotos(albumId, formData) {
|
||||
return fetchAdminFormData(
|
||||
`/api/admin/albums/${albumId}/photos`,
|
||||
formData,
|
||||
"POST"
|
||||
);
|
||||
}
|
||||
|
||||
// 앨범 사진 삭제
|
||||
export async function deleteAlbumPhoto(albumId, photoId) {
|
||||
return fetchAdminApi(`/api/admin/albums/${albumId}/photos/${photoId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
34
frontend/src/api/admin/bots.js
Normal file
34
frontend/src/api/admin/bots.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 어드민 봇 관리 API
|
||||
*/
|
||||
import { fetchAdminApi } from "../index";
|
||||
|
||||
// 봇 목록 조회
|
||||
export async function getBots() {
|
||||
return fetchAdminApi("/api/admin/bots");
|
||||
}
|
||||
|
||||
// 봇 시작
|
||||
export async function startBot(id) {
|
||||
return fetchAdminApi(`/api/admin/bots/${id}/start`, { method: "POST" });
|
||||
}
|
||||
|
||||
// 봇 정지
|
||||
export async function stopBot(id) {
|
||||
return fetchAdminApi(`/api/admin/bots/${id}/stop`, { method: "POST" });
|
||||
}
|
||||
|
||||
// 봇 전체 동기화
|
||||
export async function syncAllVideos(id) {
|
||||
return fetchAdminApi(`/api/admin/bots/${id}/sync-all`, { method: "POST" });
|
||||
}
|
||||
|
||||
// 할당량 경고 조회
|
||||
export async function getQuotaWarning() {
|
||||
return fetchAdminApi("/api/admin/quota-warning");
|
||||
}
|
||||
|
||||
// 할당량 경고 해제
|
||||
export async function dismissQuotaWarning() {
|
||||
return fetchAdminApi("/api/admin/quota-warning", { method: "DELETE" });
|
||||
}
|
||||
40
frontend/src/api/admin/categories.js
Normal file
40
frontend/src/api/admin/categories.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 어드민 카테고리 관리 API
|
||||
*/
|
||||
import { fetchAdminApi } from "../index";
|
||||
|
||||
// 카테고리 목록 조회
|
||||
export async function getCategories() {
|
||||
return fetchAdminApi("/api/admin/schedule-categories");
|
||||
}
|
||||
|
||||
// 카테고리 생성
|
||||
export async function createCategory(data) {
|
||||
return fetchAdminApi("/api/admin/schedule-categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// 카테고리 수정
|
||||
export async function updateCategory(id, data) {
|
||||
return fetchAdminApi(`/api/admin/schedule-categories/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// 카테고리 삭제
|
||||
export async function deleteCategory(id) {
|
||||
return fetchAdminApi(`/api/admin/schedule-categories/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
// 카테고리 순서 변경
|
||||
export async function reorderCategories(orderedIds) {
|
||||
return fetchAdminApi("/api/admin/schedule-categories/reorder", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ orderedIds }),
|
||||
});
|
||||
}
|
||||
19
frontend/src/api/admin/members.js
Normal file
19
frontend/src/api/admin/members.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 어드민 멤버 관리 API
|
||||
*/
|
||||
import { fetchAdminApi, fetchAdminFormData } from "../index";
|
||||
|
||||
// 멤버 목록 조회
|
||||
export async function getMembers() {
|
||||
return fetchAdminApi("/api/admin/members");
|
||||
}
|
||||
|
||||
// 멤버 상세 조회
|
||||
export async function getMember(id) {
|
||||
return fetchAdminApi(`/api/admin/members/${id}`);
|
||||
}
|
||||
|
||||
// 멤버 수정
|
||||
export async function updateMember(id, formData) {
|
||||
return fetchAdminFormData(`/api/admin/members/${id}`, formData, "PUT");
|
||||
}
|
||||
36
frontend/src/api/admin/schedules.js
Normal file
36
frontend/src/api/admin/schedules.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 어드민 일정 관리 API
|
||||
*/
|
||||
import { fetchAdminApi, fetchAdminFormData } from "../index";
|
||||
|
||||
// 일정 목록 조회 (월별)
|
||||
export async function getSchedules(year, month) {
|
||||
return fetchAdminApi(`/api/admin/schedules?year=${year}&month=${month}`);
|
||||
}
|
||||
|
||||
// 일정 검색 (Meilisearch)
|
||||
export async function searchSchedules(query) {
|
||||
return fetchAdminApi(
|
||||
`/api/admin/schedules/search?q=${encodeURIComponent(query)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 일정 상세 조회
|
||||
export async function getSchedule(id) {
|
||||
return fetchAdminApi(`/api/admin/schedules/${id}`);
|
||||
}
|
||||
|
||||
// 일정 생성
|
||||
export async function createSchedule(formData) {
|
||||
return fetchAdminFormData("/api/admin/schedules", formData, "POST");
|
||||
}
|
||||
|
||||
// 일정 수정
|
||||
export async function updateSchedule(id, formData) {
|
||||
return fetchAdminFormData(`/api/admin/schedules/${id}`, formData, "PUT");
|
||||
}
|
||||
|
||||
// 일정 삭제
|
||||
export async function deleteSchedule(id) {
|
||||
return fetchAdminApi(`/api/admin/schedules/${id}`, { method: "DELETE" });
|
||||
}
|
||||
19
frontend/src/api/albums.js
Normal file
19
frontend/src/api/albums.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 앨범 관련 공개 API
|
||||
*/
|
||||
import { fetchApi } from "./index";
|
||||
|
||||
// 앨범 목록 조회
|
||||
export async function getAlbums() {
|
||||
return fetchApi("/api/albums");
|
||||
}
|
||||
|
||||
// 앨범 상세 조회
|
||||
export async function getAlbum(id) {
|
||||
return fetchApi(`/api/albums/${id}`);
|
||||
}
|
||||
|
||||
// 앨범 사진 조회
|
||||
export async function getAlbumPhotos(albumId) {
|
||||
return fetchApi(`/api/albums/${albumId}/photos`);
|
||||
}
|
||||
60
frontend/src/api/index.js
Normal file
60
frontend/src/api/index.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* 공통 API 유틸리티
|
||||
* 모든 API 호출에서 사용되는 기본 fetch 래퍼
|
||||
*/
|
||||
|
||||
// 기본 fetch 래퍼
|
||||
export async function fetchApi(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: "요청 실패" }));
|
||||
throw new Error(error.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 어드민 토큰 가져오기
|
||||
export function getAdminToken() {
|
||||
return localStorage.getItem("adminToken");
|
||||
}
|
||||
|
||||
// 어드민 API용 fetch 래퍼 (토큰 자동 추가)
|
||||
export async function fetchAdminApi(url, options = {}) {
|
||||
const token = getAdminToken();
|
||||
|
||||
return fetchApi(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// FormData 전송용 (이미지 업로드 등)
|
||||
export async function fetchAdminFormData(url, formData, method = "POST") {
|
||||
const token = getAdminToken();
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: "요청 실패" }));
|
||||
throw new Error(error.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
14
frontend/src/api/members.js
Normal file
14
frontend/src/api/members.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 멤버 관련 공개 API
|
||||
*/
|
||||
import { fetchApi } from "./index";
|
||||
|
||||
// 멤버 목록 조회
|
||||
export async function getMembers() {
|
||||
return fetchApi("/api/members");
|
||||
}
|
||||
|
||||
// 멤버 상세 조회
|
||||
export async function getMember(id) {
|
||||
return fetchApi(`/api/members/${id}`);
|
||||
}
|
||||
28
frontend/src/api/schedules.js
Normal file
28
frontend/src/api/schedules.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 일정 관련 공개 API
|
||||
*/
|
||||
import { fetchApi } from "./index";
|
||||
|
||||
// 일정 목록 조회 (월별)
|
||||
export async function getSchedules(year, month) {
|
||||
return fetchApi(`/api/schedules?year=${year}&month=${month}`);
|
||||
}
|
||||
|
||||
// 일정 검색 (Meilisearch)
|
||||
export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) {
|
||||
return fetchApi(
|
||||
`/api/schedules?search=${encodeURIComponent(
|
||||
query
|
||||
)}&offset=${offset}&limit=${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
// 일정 상세 조회
|
||||
export async function getSchedule(id) {
|
||||
return fetchApi(`/api/schedules/${id}`);
|
||||
}
|
||||
|
||||
// 카테고리 목록 조회
|
||||
export async function getCategories() {
|
||||
return fetchApi("/api/schedule-categories");
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { Clock, ChevronLeft, ChevronRight, ChevronDown, Tag, Search, ArrowLeft,
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { getTodayKST } from '../../utils/date';
|
||||
import { getSchedules, getCategories, searchSchedules as searchSchedulesApi } from '../../api/schedules';
|
||||
|
||||
function Schedule() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -84,24 +85,21 @@ function Schedule() {
|
|||
// 데이터 로드
|
||||
// 초기 데이터 로드 (카테고리만)
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
// 월 변경 시 일정 로드
|
||||
useEffect(() => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
fetchSchedules(year, month + 1);
|
||||
loadSchedules(year, month + 1);
|
||||
}, [currentDate]);
|
||||
|
||||
const fetchSchedules = async (year, month) => {
|
||||
const loadSchedules = async (year, month) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/schedules?year=${year}&month=${month}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSchedules(data);
|
||||
}
|
||||
const data = await getSchedules(year, month);
|
||||
setSchedules(data);
|
||||
} catch (error) {
|
||||
console.error('일정 로드 오류:', error);
|
||||
} finally {
|
||||
|
|
@ -109,13 +107,10 @@ function Schedule() {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/schedule-categories');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCategories(data);
|
||||
}
|
||||
const data = await getCategories();
|
||||
setCategories(data);
|
||||
} catch (error) {
|
||||
console.error('카테고리 로드 오류:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import * as botsApi from '../../../api/admin/bots';
|
||||
|
||||
function AdminScheduleBots() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -44,15 +45,8 @@ function AdminScheduleBots() {
|
|||
const fetchBots = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const response = await fetch('/api/admin/bots', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBots(data);
|
||||
}
|
||||
const data = await botsApi.getBots();
|
||||
setBots(data);
|
||||
} catch (error) {
|
||||
console.error('봇 목록 조회 오류:', error);
|
||||
setToast({ type: 'error', message: '봇 목록을 불러올 수 없습니다.' });
|
||||
|
|
@ -64,15 +58,9 @@ function AdminScheduleBots() {
|
|||
// 할당량 경고 상태 조회
|
||||
const fetchQuotaWarning = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const response = await fetch('/api/admin/quota-warning', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.active) {
|
||||
setQuotaWarning(data);
|
||||
}
|
||||
const data = await botsApi.getQuotaWarning();
|
||||
if (data.active) {
|
||||
setQuotaWarning(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할당량 경고 조회 오류:', error);
|
||||
|
|
@ -80,13 +68,9 @@ function AdminScheduleBots() {
|
|||
};
|
||||
|
||||
// 할당량 경고 해제
|
||||
const dismissQuotaWarning = async () => {
|
||||
const handleDismissQuotaWarning = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch('/api/admin/quota-warning', {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
await botsApi.dismissQuotaWarning();
|
||||
setQuotaWarning(null);
|
||||
} catch (error) {
|
||||
console.error('할당량 경고 해제 오류:', error);
|
||||
|
|
@ -102,61 +86,44 @@ function AdminScheduleBots() {
|
|||
// 봇 시작/정지 토글
|
||||
const toggleBot = async (botId, currentStatus, botName) => {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const action = currentStatus === 'running' ? 'stop' : 'start';
|
||||
|
||||
const response = await fetch(`/api/admin/bots/${botId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// 로컬 상태만 업데이트 (전체 목록 새로고침 대신)
|
||||
setBots(prev => prev.map(bot =>
|
||||
bot.id === botId
|
||||
? { ...bot, status: action === 'start' ? 'running' : 'stopped' }
|
||||
: bot
|
||||
));
|
||||
setToast({
|
||||
type: 'success',
|
||||
message: action === 'start' ? `${botName} 봇이 시작되었습니다.` : `${botName} 봇이 정지되었습니다.`
|
||||
});
|
||||
if (action === 'start') {
|
||||
await botsApi.startBot(botId);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setToast({ type: 'error', message: data.error || '작업 실패' });
|
||||
await botsApi.stopBot(botId);
|
||||
}
|
||||
|
||||
// 로컬 상태만 업데이트 (전체 목록 새로고침 대신)
|
||||
setBots(prev => prev.map(bot =>
|
||||
bot.id === botId
|
||||
? { ...bot, status: action === 'start' ? 'running' : 'stopped' }
|
||||
: bot
|
||||
));
|
||||
setToast({
|
||||
type: 'success',
|
||||
message: action === 'start' ? `${botName} 봇이 시작되었습니다.` : `${botName} 봇이 정지되었습니다.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('봇 토글 오류:', error);
|
||||
setToast({ type: 'error', message: '작업 중 오류가 발생했습니다.' });
|
||||
setToast({ type: 'error', message: error.message || '작업 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 전체 동기화
|
||||
const syncAllVideos = async (botId) => {
|
||||
const handleSyncAllVideos = async (botId) => {
|
||||
setSyncing(botId);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const response = await fetch(`/api/admin/bots/${botId}/sync-all`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
const data = await botsApi.syncAllVideos(botId);
|
||||
setToast({
|
||||
type: 'success',
|
||||
message: `${data.addedCount}개 일정이 추가되었습니다. (전체 ${data.total}개)`
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setToast({
|
||||
type: 'success',
|
||||
message: `${data.addedCount}개 일정이 추가되었습니다. (전체 ${data.total}개)`
|
||||
});
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setToast({ type: 'error', message: data.error || '동기화 실패' });
|
||||
}
|
||||
// 성공/실패 모두 목록 갱신
|
||||
fetchBots();
|
||||
} catch (error) {
|
||||
console.error('전체 동기화 오류:', error);
|
||||
setToast({ type: 'error', message: '동기화 중 오류가 발생했습니다.' });
|
||||
fetchBots(); // 오류에도 목록 갱신
|
||||
setToast({ type: 'error', message: error.message || '동기화 중 오류가 발생했습니다.' });
|
||||
fetchBots();
|
||||
} finally {
|
||||
setSyncing(null);
|
||||
}
|
||||
|
|
@ -320,7 +287,7 @@ function AdminScheduleBots() {
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={dismissQuotaWarning}
|
||||
onClick={handleDismissQuotaWarning}
|
||||
className="text-red-400 hover:text-red-600 transition-colors text-sm px-2 py-1"
|
||||
>
|
||||
닫기
|
||||
|
|
@ -418,7 +385,7 @@ function AdminScheduleBots() {
|
|||
<div className="p-4 border-t border-gray-100">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => syncAllVideos(bot.id)}
|
||||
onClick={() => handleSyncAllVideos(bot.id)}
|
||||
disabled={syncing === bot.id}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-500 text-white rounded-lg font-medium transition-colors hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue