From 9886048a4c213b746311fa22ef9bb10482226ccf Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 9 Jan 2026 21:56:32 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20API=20=EC=A4=91=EC=95=99=ED=99=94?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=20(1/3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/ 디렉토리 구조 생성 - index.js: 공통 fetch 래퍼 - schedules.js, albums.js, members.js: 공개 API - admin/: 어드민 API (bots, albums, categories, members, schedules) - Schedule.jsx: API 모듈 적용 - AdminScheduleBots.jsx: API 모듈 적용 --- frontend/src/api/admin/albums.js | 50 ++++++++++ frontend/src/api/admin/bots.js | 34 +++++++ frontend/src/api/admin/categories.js | 40 ++++++++ frontend/src/api/admin/members.js | 19 ++++ frontend/src/api/admin/schedules.js | 36 +++++++ frontend/src/api/albums.js | 19 ++++ frontend/src/api/index.js | 60 ++++++++++++ frontend/src/api/members.js | 14 +++ frontend/src/api/schedules.js | 28 ++++++ frontend/src/pages/pc/Schedule.jsx | 23 ++--- .../src/pages/pc/admin/AdminScheduleBots.jsx | 97 ++++++------------- 11 files changed, 341 insertions(+), 79 deletions(-) create mode 100644 frontend/src/api/admin/albums.js create mode 100644 frontend/src/api/admin/bots.js create mode 100644 frontend/src/api/admin/categories.js create mode 100644 frontend/src/api/admin/members.js create mode 100644 frontend/src/api/admin/schedules.js create mode 100644 frontend/src/api/albums.js create mode 100644 frontend/src/api/index.js create mode 100644 frontend/src/api/members.js create mode 100644 frontend/src/api/schedules.js diff --git a/frontend/src/api/admin/albums.js b/frontend/src/api/admin/albums.js new file mode 100644 index 0000000..9235682 --- /dev/null +++ b/frontend/src/api/admin/albums.js @@ -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", + }); +} diff --git a/frontend/src/api/admin/bots.js b/frontend/src/api/admin/bots.js new file mode 100644 index 0000000..0bc1828 --- /dev/null +++ b/frontend/src/api/admin/bots.js @@ -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" }); +} diff --git a/frontend/src/api/admin/categories.js b/frontend/src/api/admin/categories.js new file mode 100644 index 0000000..c584830 --- /dev/null +++ b/frontend/src/api/admin/categories.js @@ -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 }), + }); +} diff --git a/frontend/src/api/admin/members.js b/frontend/src/api/admin/members.js new file mode 100644 index 0000000..cda7a06 --- /dev/null +++ b/frontend/src/api/admin/members.js @@ -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"); +} diff --git a/frontend/src/api/admin/schedules.js b/frontend/src/api/admin/schedules.js new file mode 100644 index 0000000..433c721 --- /dev/null +++ b/frontend/src/api/admin/schedules.js @@ -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" }); +} diff --git a/frontend/src/api/albums.js b/frontend/src/api/albums.js new file mode 100644 index 0000000..074cfe8 --- /dev/null +++ b/frontend/src/api/albums.js @@ -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`); +} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..7af8c48 --- /dev/null +++ b/frontend/src/api/index.js @@ -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(); +} diff --git a/frontend/src/api/members.js b/frontend/src/api/members.js new file mode 100644 index 0000000..a95b5f5 --- /dev/null +++ b/frontend/src/api/members.js @@ -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}`); +} diff --git a/frontend/src/api/schedules.js b/frontend/src/api/schedules.js new file mode 100644 index 0000000..aa74b91 --- /dev/null +++ b/frontend/src/api/schedules.js @@ -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"); +} diff --git a/frontend/src/pages/pc/Schedule.jsx b/frontend/src/pages/pc/Schedule.jsx index 16fa54f..3930ccd 100644 --- a/frontend/src/pages/pc/Schedule.jsx +++ b/frontend/src/pages/pc/Schedule.jsx @@ -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); } diff --git a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx index be9b35f..0d5049c 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx @@ -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() {