guardia-esn/frontend/src/pages/UserList.tsx

182 lines
7.9 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Edit, Trash2, X } from 'lucide-react'
import { getUsers, createUser, updateUser, deleteUser } from '../api/client'
export default function UserList() {
const qc = useQueryClient()
const [modal, setModal] = useState<any>(null)
const { data: users = [], isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => getUsers(),
})
const saveMut = useMutation({
mutationFn: (d: any) => d.id ? updateUser(d.id, d) : createUser(d),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }); setModal(null) },
})
const deleteMut = useMutation({
mutationFn: deleteUser,
onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),
})
if (isLoading) return <div className="text-gray-400 p-8"> ...</div>
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-white"> </h1>
<button onClick={() => setModal({})}
className="flex items-center gap-1.5 bg-brand hover:bg-brand2 text-white px-3 py-1.5 rounded text-sm">
<Plus size={14} />
</button>
</div>
<div className="bg-card border border-edge rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-panel border-b border-edge">
<tr>
{['아이디','테넌트','역할','이메일','전화','상태','마지막 로그인','액션'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
))}
</tr>
</thead>
<tbody>
{(users as any[]).map((u: any) => (
<tr key={u.id} className="border-b border-edge hover:bg-edge/30">
<td className="px-4 py-3 text-white font-medium">{u.username}</td>
<td className="px-4 py-3 text-brand text-xs">{u.tenantCode || 'SUPER'}</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded text-xs ${
u.role === 'ADMIN' ? 'bg-red-500/20 text-red-400' :
u.role === 'MANAGER' ? 'bg-brand/20 text-brand' :
'bg-gray-500/20 text-gray-400'
}`}>{u.role}</span>
</td>
<td className="px-4 py-3 text-gray-400 text-xs">{u.email || '-'}</td>
<td className="px-4 py-3 text-gray-400 text-xs">{u.phone || '-'}</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded text-xs ${u.active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}`}>
{u.active ? '활성' : '비활성'}
</span>
</td>
<td className="px-4 py-3 text-gray-500 text-xs">
{u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString('ko-KR') : '-'}
</td>
<td className="px-4 py-3">
<div className="flex gap-2">
<button onClick={() => setModal(u)} className="text-brand hover:text-blue-300"><Edit size={14} /></button>
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(u.id) }}
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
</div>
</td>
</tr>
))}
{(users as any[]).length === 0 && (
<tr><td colSpan={8} className="px-4 py-8 text-center text-gray-500"> </td></tr>
)}
</tbody>
</table>
</div>
{modal && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-card border border-edge rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between mb-4">
<h2 className="font-semibold">{modal.id ? '사용자 수정' : '사용자 추가'}</h2>
<button onClick={() => setModal(null)}><X size={16} /></button>
</div>
<UserForm initial={modal} onSave={(d: any) => saveMut.mutate(d)} />
</div>
</div>
)}
</div>
)
}
function UserForm({ initial, onSave }: { initial: any; onSave: (d: any) => void }) {
const isEdit = !!initial.id
const [f, setF] = useState({
username: initial.username || '',
role: initial.role || 'USER',
tenantCode: initial.tenantCode || '',
email: initial.email || '',
phone: initial.phone || '',
password: '',
active: initial.active !== false,
id: initial.id,
})
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
setF(p => ({ ...p, [k]: e.target.value }))
const submit = (e: React.FormEvent) => {
e.preventDefault()
const payload: any = {
username: f.username,
role: f.role,
tenantCode: f.tenantCode || null,
email: f.email || null,
phone: f.phone || null,
active: f.active === true || (f as any).active === 'true',
id: f.id,
}
// 신규 생성 시에만 비밀번호 포함(미입력 시 백엔드 기본값 사용)
if (!isEdit && f.password) payload.password = f.password
onSave(payload)
}
return (
<form onSubmit={submit} className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1"></label>
<input value={f.username} onChange={set('username')} disabled={isEdit} required
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white disabled:opacity-50" />
</div>
{!isEdit && (
<div>
<label className="block text-xs text-gray-400 mb-1"> ( changeme123)</label>
<input type="password" value={f.password} onChange={set('password')} autoComplete="new-password"
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
</div>
)}
<div>
<label className="block text-xs text-gray-400 mb-1"></label>
<select value={f.role} onChange={set('role')}
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
{['ADMIN','MANAGER','USER'].map(o => <option key={o} value={o}>{o}</option>)}
</select>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1"> (SUPER는 )</label>
<select value={f.tenantCode} onChange={set('tenantCode')}
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
<option value="">(SUPER / )</option>
{['TENANT_A','TENANT_B','EMART','ZIOINFO'].map(o => <option key={o} value={o}>{o}</option>)}
</select>
</div>
{[
{ label: '이메일', key: 'email', type: 'email' },
{ label: '전화', key: 'phone' },
].map(({ label, key, type }) => (
<div key={key}>
<label className="block text-xs text-gray-400 mb-1">{label}</label>
<input type={type || 'text'} value={(f as any)[key]} onChange={set(key)}
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
</div>
))}
<div>
<label className="block text-xs text-gray-400 mb-1"></label>
<select value={String(f.active)} onChange={set('active')}
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<button type="submit" className="w-full bg-brand hover:bg-brand2 text-white py-2 rounded text-sm">
</button>
</form>
)
}