195 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|