- 멤버 수정 페이지 추가 (AdminMemberEdit.jsx) - 커스텀 데이트픽커 적용 (앨범 폼과 동일) - 활동 상태 버튼 토글 (활동 중/탈퇴) - URL 라우터 멤버 이름 기반으로 변경 (/admin/members/:name/edit) - 백엔드 멤버 조회/수정 API 추가 (이름 기반) - 전 멤버 섹션 UI 개선 (배경 항상 표시, 포지션 제거)
212 lines
9.3 KiB
JavaScript
212 lines
9.3 KiB
JavaScript
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 }) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{
|
|
type: 'tween',
|
|
ease: 'easeOut',
|
|
duration: 0.35,
|
|
delay: index * 0.06
|
|
}}
|
|
className={`relative rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all group cursor-pointer ${isFormer ? 'opacity-60' : ''}`}
|
|
onClick={() => navigate(`/admin/members/${encodeURIComponent(member.name)}/edit`)}
|
|
>
|
|
{/* 프로필 이미지 */}
|
|
<div className={`aspect-[3/4] bg-gray-100 relative overflow-hidden ${isFormer ? 'grayscale' : ''}`}>
|
|
{member.image_url ? (
|
|
<img
|
|
src={member.image_url}
|
|
alt={member.name}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-gradient-to-b from-gray-100 to-gray-200">
|
|
<User size={48} className="text-gray-300" />
|
|
</div>
|
|
)}
|
|
|
|
{/* 이름 오버레이 - 하단 그라데이션 */}
|
|
<div className="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
|
|
<div className="absolute bottom-0 left-0 right-0 p-4">
|
|
<h3 className="text-lg font-bold text-white drop-shadow-lg">{member.name}</h3>
|
|
</div>
|
|
|
|
{/* 수정 버튼 오버레이 */}
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
|
|
<div className="px-4 py-2 bg-white/90 backdrop-blur-sm text-gray-900 rounded-lg font-medium flex items-center gap-2 shadow-lg">
|
|
<Edit2 size={16} />
|
|
수정
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Toast */}
|
|
<Toast toast={toast} onClose={() => setToast(null)} />
|
|
|
|
{/* 헤더 */}
|
|
<header className="bg-white shadow-sm border-b border-gray-100">
|
|
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link to="/admin/dashboard" className="text-2xl font-bold text-primary hover:opacity-80 transition-opacity">
|
|
fromis_9
|
|
</Link>
|
|
<span className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full">
|
|
Admin
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-gray-500 text-sm">
|
|
안녕하세요, <span className="text-gray-900 font-medium">{user?.username}</span>님
|
|
</span>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-2 px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<LogOut size={18} />
|
|
<span>로그아웃</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 메인 콘텐츠 */}
|
|
<main className="max-w-7xl mx-auto px-6 py-8">
|
|
{/* 브레드크럼 */}
|
|
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
|
|
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
|
|
<Home size={16} />
|
|
</Link>
|
|
<ChevronRight size={14} />
|
|
<span className="text-gray-700">멤버 관리</span>
|
|
</div>
|
|
|
|
{/* 타이틀 */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">멤버 관리</h1>
|
|
<p className="text-gray-500">멤버 정보 및 프로필을 관리합니다</p>
|
|
</div>
|
|
|
|
{/* 멤버 목록 */}
|
|
{loading ? (
|
|
<div className="flex justify-center items-center py-20">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-12">
|
|
{/* 활동 멤버 */}
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<Users size={20} className="text-primary" />
|
|
<h2 className="text-xl font-bold text-gray-900">현재 멤버</h2>
|
|
<span className="px-2 py-0.5 bg-primary/10 text-primary text-sm font-medium rounded-full">
|
|
{activeMembers.length}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 5열 그리드 */}
|
|
<div className="grid grid-cols-5 gap-5">
|
|
{activeMembers.map((member, index) => (
|
|
<MemberCard key={member.id} member={member} index={index} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 탈퇴 멤버 */}
|
|
{formerMembers.length > 0 && (
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<User size={20} className="text-gray-400" />
|
|
<h2 className="text-xl font-bold text-gray-500">이전 멤버</h2>
|
|
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-sm font-medium rounded-full">
|
|
{formerMembers.length}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 5열 그리드 (탈퇴 멤버용 - 4명이면 4개만 표시) */}
|
|
<div className="grid grid-cols-5 gap-5">
|
|
{formerMembers.map((member, index) => (
|
|
<MemberCard key={member.id} member={member} index={index} isFormer />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{members.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
등록된 멤버가 없습니다.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AdminMembers;
|