From 97e63580e5819b39a9fe2f765822f1e592d16d95 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 4 Jan 2026 13:10:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자 페이지 헤더 로고 클릭 시 어드민 대시보드로 이동 - 멤버 관리 페이지 UI 추가 (5열 그리드, 현재/이전 멤버 분리) - 대시보드 통계 실제 데이터 연결 및 슬롯머신 애니메이션 - 멤버 카드 페이드+스케일업 애니메이션 --- frontend/src/App.jsx | 2 + .../src/pages/pc/admin/AdminAlbumForm.jsx | 2 +- .../src/pages/pc/admin/AdminAlbumPhotos.jsx | 2 +- frontend/src/pages/pc/admin/AdminAlbums.jsx | 2 +- .../src/pages/pc/admin/AdminDashboard.jsx | 103 ++++++++- frontend/src/pages/pc/admin/AdminMembers.jsx | 212 ++++++++++++++++++ 6 files changed, 312 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/pc/admin/AdminMembers.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fff4dab..eecf5ec 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,7 @@ import PCSchedule from './pages/pc/Schedule'; // 관리자 페이지 import AdminLogin from './pages/pc/admin/AdminLogin'; import AdminDashboard from './pages/pc/admin/AdminDashboard'; +import AdminMembers from './pages/pc/admin/AdminMembers'; import AdminAlbums from './pages/pc/admin/AdminAlbums'; import AdminAlbumForm from './pages/pc/admin/AdminAlbumForm'; import AdminAlbumPhotos from './pages/pc/admin/AdminAlbumPhotos'; @@ -27,6 +28,7 @@ function App() { {/* 관리자 페이지 (레이아웃 없음) */} } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx index 727b133..3131f83 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx @@ -566,7 +566,7 @@ function AdminAlbumForm() {
- + fromis_9 diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx index de98d7c..bbcfa7e 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx @@ -671,7 +671,7 @@ function AdminAlbumPhotos() {
- + fromis_9 diff --git a/frontend/src/pages/pc/admin/AdminAlbums.jsx b/frontend/src/pages/pc/admin/AdminAlbums.jsx index e984d3c..c087e22 100644 --- a/frontend/src/pages/pc/admin/AdminAlbums.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbums.jsx @@ -169,7 +169,7 @@ function AdminAlbums() {
- + fromis_9 diff --git a/frontend/src/pages/pc/admin/AdminDashboard.jsx b/frontend/src/pages/pc/admin/AdminDashboard.jsx index 7bb569d..e2d22f7 100644 --- a/frontend/src/pages/pc/admin/AdminDashboard.jsx +++ b/frontend/src/pages/pc/admin/AdminDashboard.jsx @@ -6,9 +6,46 @@ import { Home, ChevronRight } from 'lucide-react'; +// 슬롯머신 스타일 롤링 숫자 컴포넌트 (아래에서 위로) +function AnimatedNumber({ value }) { + const digits = String(value).split(''); + + return ( + + {digits.map((digit, i) => ( + + + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => ( + + {n} + + ))} + + + ))} + + ); +} + function AdminDashboard() { const navigate = useNavigate(); const [user, setUser] = useState(null); + const [stats, setStats] = useState({ + albums: 0, + photos: 0, + schedules: 0, + members: 0 + }); useEffect(() => { // 로그인 상태 확인 @@ -35,8 +72,58 @@ function AdminDashboard() { localStorage.removeItem('adminUser'); navigate('/admin'); }); + + // 통계 데이터 가져오기 + fetchStats(); }, [navigate]); + 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 })); + } + } 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 })); + + // 사진 수 계산 + 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 })); + } + } 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 })); + } + } catch (e) { console.error('일정 통계 오류:', e); } + }; + const handleLogout = () => { localStorage.removeItem('adminToken'); localStorage.removeItem('adminUser'); @@ -74,7 +161,7 @@ function AdminDashboard() {
- + fromis_9 @@ -139,21 +226,21 @@ function AdminDashboard() {

빠른 통계

-

-

+

+

멤버

+
+
+

총 앨범

-

-

+

총 사진

-

-

+

총 일정

-
-

5

-

멤버

-
diff --git a/frontend/src/pages/pc/admin/AdminMembers.jsx b/frontend/src/pages/pc/admin/AdminMembers.jsx new file mode 100644 index 0000000..3ef7a40 --- /dev/null +++ b/frontend/src/pages/pc/admin/AdminMembers.jsx @@ -0,0 +1,212 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { + Edit2, LogOut, + Home, ChevronRight, Users, User +} from 'lucide-react'; +import Toast from '../../../components/Toast'; + +function AdminMembers() { + const navigate = useNavigate(); + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + const [toast, setToast] = useState(null); + + // Toast 자동 숨김 + useEffect(() => { + if (toast) { + const timer = setTimeout(() => setToast(null), 3000); + return () => clearTimeout(timer); + } + }, [toast]); + + useEffect(() => { + // 로그인 확인 + const token = localStorage.getItem('adminToken'); + const userData = localStorage.getItem('adminUser'); + + if (!token || !userData) { + navigate('/admin'); + return; + } + + setUser(JSON.parse(userData)); + fetchMembers(); + }, [navigate]); + + const fetchMembers = () => { + fetch('/api/members') + .then(res => res.json()) + .then(data => { + setMembers(data); + setLoading(false); + }) + .catch(error => { + console.error('멤버 로드 오류:', error); + setLoading(false); + }); + }; + + const handleLogout = () => { + localStorage.removeItem('adminToken'); + localStorage.removeItem('adminUser'); + navigate('/admin'); + }; + + // 활동/탈퇴 멤버 분리 (is_former: 0=활동, 1=탈퇴) + const activeMembers = members.filter(m => !m.is_former); + const formerMembers = members.filter(m => m.is_former); + + // 멤버 카드 컴포넌트 + const MemberCard = ({ member, index, isFormer = false }) => ( + setToast({ message: '수정 기능은 준비 중입니다.', type: 'info' })} + > + {/* 프로필 이미지 */} +
+ {member.image_url ? ( + {member.name} + ) : ( +
+ +
+ )} + + {/* 이름 오버레이 - 하단 그라데이션 */} +
+
+

{member.name}

+
+ + {/* 수정 버튼 오버레이 */} +
+
+ + 수정 +
+
+
+ + ); + + return ( +
+ {/* Toast */} + setToast(null)} /> + + {/* 헤더 */} +
+
+
+ + fromis_9 + + + Admin + +
+
+ + 안녕하세요, {user?.username}님 + + +
+
+
+ + {/* 메인 콘텐츠 */} +
+ {/* 브레드크럼 */} +
+ + + + + 멤버 관리 +
+ + {/* 타이틀 */} +
+

멤버 관리

+

멤버 정보 및 프로필을 관리합니다

+
+ + {/* 멤버 목록 */} + {loading ? ( +
+
+
+ ) : ( +
+ {/* 활동 멤버 */} +
+
+ +

현재 멤버

+ + {activeMembers.length} + +
+ + {/* 5열 그리드 */} +
+ {activeMembers.map((member, index) => ( + + ))} +
+
+ + {/* 탈퇴 멤버 */} + {formerMembers.length > 0 && ( +
+
+ +

이전 멤버

+ + {formerMembers.length} + +
+ + {/* 5열 그리드 (탈퇴 멤버용 - 4명이면 4개만 표시) */} +
+ {formerMembers.map((member, index) => ( + + ))} +
+
+ )} + + {members.length === 0 && ( +
+ 등록된 멤버가 없습니다. +
+ )} +
+ )} +
+
+ ); +} + +export default AdminMembers;