gc-guide/src/pages/admin/RoleManagement.tsx

195 lines
7.3 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import type { Role } from '../../types';
import { api } from '../../utils/api';
interface RoleForm {
name: string;
description: string;
}
export function RoleManagement() {
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingRole, setEditingRole] = useState<Role | null>(null);
const [form, setForm] = useState<RoleForm>({ name: '', description: '' });
const fetchRoles = useCallback(async () => {
try {
const data = await api.get<Role[]>('/admin/roles');
setRoles(data);
} catch {
// API 미연동 시 빈 배열 유지
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
const openCreate = () => {
setEditingRole(null);
setForm({ name: '', description: '' });
setModalOpen(true);
};
const openEdit = (role: Role) => {
setEditingRole(role);
setForm({ name: role.name, description: role.description });
setModalOpen(true);
};
const handleSave = async () => {
try {
if (editingRole) {
await api.put<Role>(`/admin/roles/${editingRole.id}`, form);
} else {
await api.post<Role>('/admin/roles', form);
}
setModalOpen(false);
fetchRoles();
} catch {
// 에러 처리
}
};
const handleDelete = async (roleId: number) => {
try {
await api.delete(`/admin/roles/${roleId}`);
fetchRoles();
} catch {
// 에러 처리
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<div className="animate-spin h-6 w-6 border-3 border-accent border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-text-primary"> </h1>
<button
onClick={openCreate}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent-hover cursor-pointer"
>
+
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{roles.length === 0 ? (
<div className="col-span-full text-center py-12 text-text-muted">
.
</div>
) : (
roles.map((role) => (
<div key={role.id} className="bg-surface border border-border-default rounded-xl p-5">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-text-primary">{role.name}</h3>
{role.defaultGrant && (
<span className="px-1.5 py-0.5 bg-success/10 text-success text-xs font-medium rounded">
</span>
)}
</div>
<div className="flex gap-1">
<button
onClick={() => openEdit(role)}
className="p-1.5 text-text-muted hover:text-accent cursor-pointer"
title="편집"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(role.id)}
className="p-1.5 text-text-muted hover:text-danger cursor-pointer"
title="삭제"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<p className="text-sm text-text-secondary mb-3">{role.description}</p>
<div>
<p className="text-xs font-medium text-text-muted mb-1">URL </p>
{role.urlPatterns.length > 0 ? (
<div className="flex flex-wrap gap-1">
{role.urlPatterns.map((p) => (
<span key={p} className="px-2 py-0.5 bg-bg-tertiary rounded text-xs font-mono text-text-secondary">
{p}
</span>
))}
</div>
) : (
<span className="text-xs text-text-muted"></span>
)}
</div>
</div>
))
)}
</div>
{/* 생성/편집 모달 */}
{modalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-surface rounded-xl shadow-xl max-w-md w-full p-6">
<h3 className="text-lg font-bold text-text-primary mb-4">
{editingRole ? '롤 편집' : '새 롤 생성'}
</h3>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-text-primary mb-1"></label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className="w-full px-3 py-2 border border-border-default rounded-lg text-sm bg-bg-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
placeholder="예: DEVELOPER"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1"></label>
<textarea
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
className="w-full px-3 py-2 border border-border-default rounded-lg text-sm bg-bg-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-accent resize-none"
rows={3}
placeholder="롤에 대한 설명"
/>
</div>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setModalOpen(false)}
className="px-4 py-2 text-sm text-text-secondary border border-border-default rounded-lg hover:bg-bg-tertiary cursor-pointer"
>
</button>
<button
onClick={handleSave}
disabled={!form.name.trim()}
className="px-4 py-2 text-sm bg-accent text-white rounded-lg hover:bg-accent-hover cursor-pointer disabled:opacity-50"
>
{editingRole ? '수정' : '생성'}
</button>
</div>
</div>
</div>
)}
</div>
);
}