fromis_9/frontend-temp/src/pages/pc/admin/Members.jsx
caadiq f64f6cee00 feat: 관리자 간단한 페이지 마이그레이션 (Phase 3)
- AdminDashboard 페이지 추가
- AdminMembers 페이지 추가
- AdminMemberEdit 페이지 추가
- useToast 훅 추가
- App.jsx에 관리자 라우트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:03:21 +09:00

180 lines
6.4 KiB
JavaScript

/**
* 관리자 멤버 목록 페이지
*/
import { useEffect } from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { Edit2, Home, ChevronRight, Users, User } from 'lucide-react';
import { Toast } from '@/components/common';
import { AdminLayout } from '@/components/pc/admin';
import { useAdminAuth } from '@/hooks/pc/admin';
import { useToast } from '@/hooks/common';
import { adminMemberApi } from '@/api/pc/admin';
/**
* 멤버 카드 컴포넌트
*/
function MemberCard({ member, index, isFormer = false, onClick }) {
return (
<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={onClick}
>
<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>
);
}
function AdminMembers() {
const navigate = useNavigate();
const location = useLocation();
const { user, isAuthenticated } = useAdminAuth();
const { toast, setToast } = useToast();
// 다른 페이지에서 전달된 토스트 메시지 처리
useEffect(() => {
if (location.state?.toast) {
setToast(location.state.toast);
window.history.replaceState({}, '');
}
}, [location.state, setToast]);
// 멤버 목록 조회
const {
data: members = [],
isLoading: loading,
isError,
} = useQuery({
queryKey: ['admin', 'members'],
queryFn: adminMemberApi.getMembers,
enabled: isAuthenticated,
});
// 에러 처리
useEffect(() => {
if (isError) {
setToast({ message: '멤버 목록을 불러오는데 실패했습니다.', type: 'error' });
}
}, [isError, setToast]);
// 활동/탈퇴 멤버 분리
const activeMembers = members.filter((m) => !m.is_former);
const formerMembers = members.filter((m) => m.is_former);
const handleMemberClick = (memberName) => {
navigate(`/admin/members/${encodeURIComponent(memberName)}/edit`);
};
return (
<AdminLayout user={user}>
<Toast toast={toast} onClose={() => setToast(null)} />
<div 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 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>
<div className="grid grid-cols-5 gap-5">
{activeMembers.map((member, index) => (
<MemberCard
key={member.id}
member={member}
index={index}
onClick={() => handleMemberClick(member.name)}
/>
))}
</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>
<div className="grid grid-cols-5 gap-5">
{formerMembers.map((member, index) => (
<MemberCard
key={member.id}
member={member}
index={index}
isFormer
onClick={() => handleMemberClick(member.name)}
/>
))}
</div>
</div>
)}
{members.length === 0 && (
<div className="text-center py-12 text-gray-500">등록된 멤버가 없습니다.</div>
)}
</div>
)}
</div>
</AdminLayout>
);
}
export default AdminMembers;