- useCallback → 일반 함수 + eslint-disable (초기 데이터 fetch) - ToastProvider: useCallback 프로퍼티 할당 → useMemo 객체 패턴 - SSAFYCallbackPage: eslint-disable-line 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
3.2 KiB
JavaScript
86 lines
3.2 KiB
JavaScript
// TODO: Add tests for CRUD operations (list users, update role, delete user)
|
|
import { useState, useEffect } from 'react';
|
|
import { getUsers, updateUserRole, deleteUser } from '../../api/users';
|
|
import { useAuth } from '../../context/useAuth';
|
|
import { useToast } from '../toast/useToast';
|
|
import './AdminCommon.css';
|
|
|
|
export default function UserAdmin() {
|
|
const [users, setUsers] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [fetchError, setFetchError] = useState(false);
|
|
const { user: me } = useAuth();
|
|
const toast = useToast();
|
|
|
|
const load = () => {
|
|
setLoading(true);
|
|
setFetchError(false);
|
|
getUsers()
|
|
.then(setUsers)
|
|
.catch((err) => {
|
|
console.error('유저 목록 로드 실패:', err);
|
|
setFetchError(true);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
};
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- initial data fetch on mount
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const handleRoleToggle = async (u) => {
|
|
const newRole = u.role === 'admin' ? 'user' : 'admin';
|
|
// TODO: Replace window.confirm() with a custom confirmation modal for consistent UI
|
|
if (!confirm(`${u.username}의 권한을 ${newRole}로 변경하시겠습니까?`)) return;
|
|
try {
|
|
await updateUserRole(u.id, newRole);
|
|
toast.success(`${u.username}의 권한이 ${newRole}로 변경되었습니다.`);
|
|
load();
|
|
} catch (err) {
|
|
toast.error(err.message || '권한 변경에 실패했습니다.');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (u) => {
|
|
// TODO: Replace window.confirm() with a custom confirmation modal for consistent UI
|
|
if (!confirm(`${u.username} 계정을 삭제하시겠습니까?`)) return;
|
|
try {
|
|
await deleteUser(u.id);
|
|
toast.success(`${u.username} 계정이 삭제되었습니다.`);
|
|
load();
|
|
} catch (err) {
|
|
toast.error(err.message || '삭제에 실패했습니다.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="admin-section">
|
|
<h2 className="admin-section-title">유저 관리</h2>
|
|
{fetchError && (
|
|
<div className="admin-error-block">
|
|
<p className="admin-error">유저 목록을 불러올 수 없습니다.</p>
|
|
<button className="btn-admin-secondary" onClick={load}>다시 시도</button>
|
|
</div>
|
|
)}
|
|
{loading && <p className="admin-list-empty">불러오는 중...</p>}
|
|
{!loading && users.length === 0 && <p className="admin-list-empty">등록된 유저가 없습니다.</p>}
|
|
<ul className="admin-list">
|
|
{users.map((u) => (
|
|
<li key={u.id} className="admin-list-item">
|
|
<div className="admin-list-info">
|
|
<span className="admin-list-title">{u.username}</span>
|
|
<span className={`admin-role-badge ${u.role}`}>{u.role}</span>
|
|
</div>
|
|
{u.username !== me?.username && (
|
|
<div className="admin-list-actions">
|
|
<button className="btn-admin-edit" onClick={() => handleRoleToggle(u)}>
|
|
{u.role === 'admin' ? '일반으로' : '관리자로'}
|
|
</button>
|
|
<button className="btn-admin-delete" onClick={() => handleDelete(u)}>삭제</button>
|
|
</div>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|