95 lines
6.0 KiB
TypeScript
95 lines
6.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Plus, Trash2, KeyRound, Power, ShieldCheck } from 'lucide-react'
|
|
import { CMS_ROLES } from '../components/rbac'
|
|
import {
|
|
getUsers, createUser, updateUserRole, updateUserActive, resetPassword, deleteUser,
|
|
} from '../api/client'
|
|
|
|
interface User { id: number; username: string; displayName?: string; role: string; active: boolean; createdAt: string | null }
|
|
|
|
function friendlyError(e: any): string {
|
|
const msg: string = e?.response?.data?.message || ''
|
|
if (e?.response?.status === 403) return '권한이 없습니다 (SuperAdmin 전용).'
|
|
if (/409|exist|이미/i.test(msg)) return '이미 존재하는 사용자명입니다.'
|
|
if (/last|마지막|423/i.test(msg)) return '마지막 SuperAdmin은 강등/삭제/비활성화할 수 없습니다.'
|
|
if (/404|not found/i.test(msg)) return '대상 사용자를 찾을 수 없습니다.'
|
|
return '요청을 처리하지 못했습니다. 잠시 후 다시 시도하세요.'
|
|
}
|
|
|
|
export default function UserManagement() {
|
|
const [list, setList] = useState<User[]>([])
|
|
const [form, setForm] = useState({ username: '', password: '', displayName: '', role: 'AUTHOR' })
|
|
const [show, setShow] = useState(false)
|
|
const [err, setErr] = useState('')
|
|
|
|
const load = () => getUsers().then(r => { setList(r.data?.data || []); setErr('') }).catch(e => setErr(friendlyError(e)))
|
|
useEffect(() => { load() }, [])
|
|
|
|
const wrap = async (fn: () => Promise<any>) => {
|
|
try { await fn(); setErr(''); await load() } catch (e) { setErr(friendlyError(e)) }
|
|
}
|
|
const create = async () => {
|
|
if (!form.username || !form.password) { setErr('사용자명과 비밀번호를 입력하세요.'); return }
|
|
await wrap(async () => { await createUser(form); setForm({ username: '', password: '', displayName: '', role: 'AUTHOR' }); setShow(false) })
|
|
}
|
|
const changeRole = (u: User, role: string) => wrap(() => updateUserRole(u.id, role))
|
|
const toggleActive = (u: User) => wrap(() => updateUserActive(u.id, !u.active))
|
|
const doReset = (u: User) => { const pw = window.prompt(`'${u.username}' 의 새 비밀번호`); if (pw) wrap(() => resetPassword(u.id, pw)) }
|
|
const remove = (u: User) => { if (window.confirm(`'${u.username}' 삭제?`)) wrap(() => deleteUser(u.id)) }
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-xl font-bold flex items-center gap-2"><ShieldCheck className="text-brand" size={20} /> 사용자 / 권한</h1>
|
|
<button onClick={() => setShow(!show)} className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-brand text-ink text-sm font-semibold">
|
|
<Plus size={16} /> 새 사용자
|
|
</button>
|
|
</div>
|
|
|
|
{err && <div className="bg-rose-500/10 border border-rose-500/40 text-rose-300 text-sm rounded-lg px-4 py-2.5 mb-4">{err}</div>}
|
|
|
|
{show && (
|
|
<div className="bg-card border border-edge rounded-xl p-5 mb-6 grid grid-cols-5 gap-3">
|
|
<input placeholder="사용자명" value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} className="inp" />
|
|
<input placeholder="표시 이름" value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} className="inp" />
|
|
<input placeholder="비밀번호" type="password" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} className="inp" />
|
|
<select value={form.role} onChange={e => setForm({ ...form, role: e.target.value })} className="inp">
|
|
{CMS_ROLES.map(r => <option key={r} value={r}>{r}</option>)}
|
|
</select>
|
|
<button onClick={create} className="px-4 rounded-lg bg-brand text-ink text-sm font-semibold">저장</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-card border border-edge rounded-xl overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="text-slate-500 text-xs bg-panel">
|
|
<tr><th className="text-left py-3 px-4">ID</th><th className="text-left">사용자명</th><th className="text-left">표시 이름</th><th className="text-left">역할</th><th className="text-left">상태</th><th className="text-left">생성일</th><th className="text-right px-4">작업</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{list.map(u => (
|
|
<tr key={u.id} className="border-t border-edge/50">
|
|
<td className="py-3 px-4 text-slate-500">{u.id}</td>
|
|
<td className="font-medium">{u.username}</td>
|
|
<td className="text-slate-400">{u.displayName || '-'}</td>
|
|
<td>
|
|
<select value={u.role} onChange={e => changeRole(u, e.target.value)} className="px-2 py-1 rounded-md bg-ink border border-edge text-xs focus:border-brand outline-none">
|
|
{CMS_ROLES.map(r => <option key={r} value={r}>{r}</option>)}
|
|
</select>
|
|
</td>
|
|
<td><span className={`inline-block px-2 py-0.5 rounded-md text-xs font-medium ${u.active ? 'bg-accent/15 text-accent' : 'bg-slate-500/15 text-slate-400'}`}>{u.active ? '활성' : '비활성'}</span></td>
|
|
<td className="text-slate-500 text-xs">{u.createdAt ? new Date(u.createdAt).toLocaleString('ko-KR') : '-'}</td>
|
|
<td className="px-4"><div className="flex items-center justify-end gap-3">
|
|
<button onClick={() => toggleActive(u)} title={u.active ? '비활성화' : '활성화'} className="text-slate-400 hover:text-brand"><Power size={16} /></button>
|
|
<button onClick={() => doReset(u)} title="비밀번호 재설정" className="text-slate-400 hover:text-brand"><KeyRound size={16} /></button>
|
|
<button onClick={() => remove(u)} title="삭제" className="text-rose-400 hover:text-rose-300"><Trash2 size={16} /></button>
|
|
</div></td>
|
|
</tr>
|
|
))}
|
|
{list.length === 0 && <tr><td colSpan={7} className="py-6 text-center text-slate-500">사용자가 없습니다.</td></tr>}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|