diff --git a/docs/frontend-improvement.md b/docs/frontend-improvement.md index 87049c7..32aaec1 100644 --- a/docs/frontend-improvement.md +++ b/docs/frontend-improvement.md @@ -147,10 +147,20 @@ pages/pc/admin/schedules/ ## 3. 작업 순서 -### Phase 1: 구조 정리 (비파괴적) -1. [ ] API 구조 개선 (폴더 재배치, transforms.js 생성) -2. [ ] 중복 유틸 함수 제거 (Schedules.jsx 등) -3. [ ] 컴포넌트 폴더 구조화 +### Phase 1: 구조 정리 (비파괴적) ✅ 완료 +1. [x] API 구조 개선 (폴더 재배치) + - `api/pc/`, `api/common/` → `api/public/`, `api/admin/` +2. [x] 미사용 코드 제거 + - utils/format.js, utils/date.js 미사용 함수 + - constants/index.js 미사용 상수 + - 미사용 hooks, stores +3. [x] 컴포넌트 폴더 구조화 (layout/, common/, schedule/ 등) +4. [x] CATEGORY_ID 하드코딩 제거 + - constants에서 삭제 + - 카테고리 API 데이터의 `name`으로 비교하도록 변경 +5. [x] 봇 관리 페이지 애니메이션 추가 + - 페이지 stagger 애니메이션 + - 통계 카드 AnimatedNumber ### Phase 2: 대형 파일 분리 1. [ ] Schedules.jsx 분리 @@ -159,21 +169,32 @@ pages/pc/admin/schedules/ 4. [ ] ScheduleDict.jsx 분리 5. [ ] AlbumForm.jsx 분리 -### Phase 3: 검증 -1. [ ] 모든 페이지 동작 확인 -2. [ ] 빌드 확인 +### Phase 3: 추가 개선 +1. [ ] 관리자 페이지용 에러 페이지 추가 (404, 500 등) +2. [ ] 모든 페이지 동작 확인 +3. [ ] 빌드 확인 --- -## 4. 삭제할 항목 +## 4. 삭제된 항목 -### constants/index.js에서 삭제됨 +### constants/index.js +- `CATEGORY_ID` (하드코딩 → API name 비교로 변경) - `CATEGORY_NAMES` (미사용) - `ALBUM_TYPES` (미사용) - `SOCIAL_LINKS.tiktok` (불필요) - `SOCIAL_LINKS.fancafe` (불필요) -### 삭제 예정 -- `api/pc/` 폴더 구조 (→ `api/public/`, `api/admin/`으로 변경) -- `api/pc/common/` (→ `api/public/`으로 통합) -- 각 파일의 중복 유틸 함수 +### API 구조 +- `api/pc/` 폴더 → `api/public/`, `api/admin/`으로 변경 완료 +- `api/common/` → `api/client.js`로 통합 완료 + +### utils +- `format.js`: formatNumber, formatViewCount, formatFileSize, formatDuration, truncateText +- `date.js`: nowKST, parseDateKST, isPast, isFuture + +### hooks +- useCalendar, useLightbox, useMediaQuery, useScheduleFiltering, useScheduleSearch + +### stores +- useUIStore diff --git a/frontend-temp/src/api/admin/schedules.js b/frontend-temp/src/api/admin/schedules.js index b1b9e3e..d7855c8 100644 --- a/frontend-temp/src/api/admin/schedules.js +++ b/frontend-temp/src/api/admin/schedules.js @@ -70,7 +70,7 @@ export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { * @returns {Promise} */ export async function getSchedule(id) { - return fetchAuthApi(`/admin/schedules/${id}`); + return fetchAuthApi(`/schedules/${id}`); } /** diff --git a/frontend-temp/src/api/index.js b/frontend-temp/src/api/index.js index e507e79..59ca0b4 100644 --- a/frontend-temp/src/api/index.js +++ b/frontend-temp/src/api/index.js @@ -13,3 +13,4 @@ export * as memberApi from './public/members'; // 관리자 API export * from './admin'; +export * as authApi from './admin/auth'; diff --git a/frontend-temp/src/constants/index.js b/frontend-temp/src/constants/index.js index 525006f..0ce1639 100644 --- a/frontend-temp/src/constants/index.js +++ b/frontend-temp/src/constants/index.js @@ -2,13 +2,6 @@ * 상수 정의 */ -/** 카테고리 ID */ -export const CATEGORY_ID = { - DEFAULT: 1, - YOUTUBE: 2, - X: 3, -}; - /** 공식 SNS 링크 */ export const SOCIAL_LINKS = { youtube: 'https://www.youtube.com/@fromis9_official', diff --git a/frontend-temp/src/pages/mobile/schedule/ScheduleDetail.jsx b/frontend-temp/src/pages/mobile/schedule/ScheduleDetail.jsx index 7bce5aa..a35ce68 100644 --- a/frontend-temp/src/pages/mobile/schedule/ScheduleDetail.jsx +++ b/frontend-temp/src/pages/mobile/schedule/ScheduleDetail.jsx @@ -5,7 +5,6 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react'; import Linkify from 'react-linkify'; import { getSchedule } from '@/api'; -import { CATEGORY_ID } from '@/constants'; import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTime } from '@/utils'; /** @@ -505,12 +504,12 @@ function MobileScheduleDetail() { } // 카테고리별 섹션 렌더링 - const categoryId = schedule.category?.id; + const categoryName = schedule.category?.name; const renderCategorySection = () => { - switch (categoryId) { - case CATEGORY_ID.YOUTUBE: + switch (categoryName) { + case '유튜브': return ; - case CATEGORY_ID.X: + case 'X': return ; default: return ; diff --git a/frontend-temp/src/pages/pc/admin/schedules/ScheduleBots.jsx b/frontend-temp/src/pages/pc/admin/schedules/ScheduleBots.jsx index 1026b38..74670c3 100644 --- a/frontend-temp/src/pages/pc/admin/schedules/ScheduleBots.jsx +++ b/frontend-temp/src/pages/pc/admin/schedules/ScheduleBots.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { Home, ChevronRight, @@ -20,6 +20,50 @@ import { useAdminAuth } from '@/hooks/pc/admin'; import { useToast } from '@/hooks/common'; import * as botsApi from '@/api/admin/bots'; +// 애니메이션 variants +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1 }, + }, +}; + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: 'easeOut' }, + }, +}; + +// 슬롯머신 스타일 롤링 숫자 컴포넌트 +function AnimatedNumber({ value, className = '' }) { + const chars = String(value).split(''); + + return ( + + {chars.map((char, i) => ( + + + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => ( + + {n} + + ))} + + + ))} + + ); +} + // X 아이콘 컴포넌트 const XIcon = ({ size = 20, fill = 'currentColor' }) => ( @@ -246,14 +290,24 @@ function ScheduleBots() { return `${minutes}분`; }; + // 통계 계산 + const runningCount = bots.filter((b) => b.status === 'running').length; + const stoppedCount = bots.filter((b) => b.status === 'stopped').length; + const errorCount = bots.filter((b) => b.status === 'error').length; + return ( setToast(null)} /> {/* 메인 콘텐츠 */} -
+ {/* 브레드크럼 */} -
+ @@ -263,63 +317,72 @@ function ScheduleBots() { 봇 관리 -
+
{/* 타이틀 */} -
+

봇 관리

일정 자동화 봇을 관리합니다

-
+ {/* 봇 통계 */} -
+
전체 봇
-
{bots.length}
+
+ +
실행 중
- {bots.filter((b) => b.status === 'running').length} +
정지됨
- {bots.filter((b) => b.status === 'stopped').length} +
오류
- {bots.filter((b) => b.status === 'error').length} +
-
+ {/* API 할당량 경고 배너 */} - {quotaWarning && ( -
-
-
- -
-
-

YouTube API 할당량 경고

-

{quotaWarning.message}

-
-
- -
- )} +
+
+ +
+
+

YouTube API 할당량 경고

+

{quotaWarning.message}

+
+
+ + + )} + {/* 봇 목록 */} -
+

봇 목록

@@ -498,8 +561,8 @@ function ScheduleBots() { })}
)} -
-
+ + ); } diff --git a/frontend-temp/src/pages/pc/admin/schedules/Schedules.jsx b/frontend-temp/src/pages/pc/admin/schedules/Schedules.jsx index c01fb38..e82ac36 100644 --- a/frontend-temp/src/pages/pc/admin/schedules/Schedules.jsx +++ b/frontend-temp/src/pages/pc/admin/schedules/Schedules.jsx @@ -118,18 +118,12 @@ const getCategoryInfo = (schedule, categories) => { return found || { id: catId, name: '미분류', color: '#6b7280' }; }; -// 카테고리 ID 상수 -const CATEGORY_IDS = { - YOUTUBE: 2, - X: 3, -}; - // 카테고리별 수정 경로 반환 -const getEditPath = (scheduleId, categoryId) => { - switch (categoryId) { - case CATEGORY_IDS.YOUTUBE: +const getEditPath = (scheduleId, categoryName) => { + switch (categoryName) { + case '유튜브': return `/admin/schedule/${scheduleId}/edit/youtube`; - case CATEGORY_IDS.X: + case 'X': return `/admin/schedule/${scheduleId}/edit/x`; default: return `/admin/schedule/${scheduleId}/edit`; @@ -230,7 +224,7 @@ const ScheduleItem = memo(function ScheduleItem({ )}