Merge pull request 'feat: 관리자 가입 설정 페이지 추가' (#8) from develop into main
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 15s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 15s
Reviewed-on: #8
This commit is contained in:
커밋
0ed69e4f65
@ -13,6 +13,7 @@ import { UserManagement } from './pages/admin/UserManagement';
|
||||
import { RoleManagement } from './pages/admin/RoleManagement';
|
||||
import { PermissionManagement } from './pages/admin/PermissionManagement';
|
||||
import { StatsPage } from './pages/admin/StatsPage';
|
||||
import { SettingsPage } from './pages/admin/SettingsPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -36,6 +37,7 @@ function App() {
|
||||
<Route path="/admin/users" element={<UserManagement />} />
|
||||
<Route path="/admin/roles" element={<RoleManagement />} />
|
||||
<Route path="/admin/permissions" element={<PermissionManagement />} />
|
||||
<Route path="/admin/settings" element={<SettingsPage />} />
|
||||
<Route path="/admin/stats" element={<StatsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@ -12,11 +12,11 @@ import { AuthContext } from './AuthContext';
|
||||
const DEV_MOCK_USER: User = {
|
||||
id: 1,
|
||||
email: 'htlee@gcsc.co.kr',
|
||||
name: '이현태 (DEV)',
|
||||
name: '김개발 (DEV)',
|
||||
avatarUrl: null,
|
||||
status: 'ACTIVE',
|
||||
isAdmin: true,
|
||||
roles: [{ id: 1, name: 'ADMIN', description: '관리자', urlPatterns: ['/**'] }],
|
||||
roles: [{ id: 1, name: 'ADMIN', description: '관리자', urlPatterns: ['/**'], defaultGrant: false }],
|
||||
createdAt: new Date().toISOString(),
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@ -93,7 +93,14 @@ export function RoleManagement() {
|
||||
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">
|
||||
<h3 className="font-semibold text-text-primary">{role.name}</h3>
|
||||
<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)}
|
||||
|
||||
168
src/pages/admin/SettingsPage.tsx
Normal file
168
src/pages/admin/SettingsPage.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { RegistrationSettings, Role } from '../../types';
|
||||
import { api } from '../../utils/api';
|
||||
import { Alert } from '../../components/common/Alert';
|
||||
|
||||
export function SettingsPage() {
|
||||
const [settings, setSettings] = useState<RegistrationSettings | null>(null);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [settingsData, rolesData] = await Promise.all([
|
||||
api.get<RegistrationSettings>('/admin/settings/registration'),
|
||||
api.get<Role[]>('/admin/roles'),
|
||||
]);
|
||||
setSettings(settingsData);
|
||||
setRoles(rolesData);
|
||||
} catch {
|
||||
// API 미연동 시 기본값
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleToggleAutoApprove = useCallback(async () => {
|
||||
if (!settings) return;
|
||||
const newValue = !settings.autoApprove;
|
||||
setSaving(true);
|
||||
try {
|
||||
const updated = await api.put<RegistrationSettings>('/admin/settings/registration', {
|
||||
autoApprove: newValue,
|
||||
});
|
||||
setSettings(updated);
|
||||
} catch {
|
||||
// 에러 처리
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const handleToggleDefaultGrant = useCallback(async (roleId: number, currentValue: boolean) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.put(`/admin/roles/${roleId}/default-grant`, {
|
||||
defaultGrant: !currentValue,
|
||||
});
|
||||
setRoles((prev) =>
|
||||
prev.map((r) => (r.id === roleId ? { ...r, defaultGrant: !currentValue } : r))
|
||||
);
|
||||
if (settings) {
|
||||
setSettings((prev) => {
|
||||
if (!prev) return prev;
|
||||
const updatedDefaultRoles = !currentValue
|
||||
? [...prev.defaultRoles, roles.find((r) => r.id === roleId)!]
|
||||
: prev.defaultRoles.filter((r) => r.id !== roleId);
|
||||
return { ...prev, defaultRoles: updatedDefaultRoles };
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 에러 처리
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [settings, roles]);
|
||||
|
||||
const isAutoApprove = settings?.autoApprove ?? false;
|
||||
const defaultGrantCount = roles.filter((r) => r.defaultGrant).length;
|
||||
|
||||
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">
|
||||
<h1 className="text-2xl font-bold text-text-primary mb-6">설정</h1>
|
||||
|
||||
<div className="max-w-2xl">
|
||||
<div className="bg-surface border border-border-default rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">가입 정책</h2>
|
||||
|
||||
{/* 자동 승인 토글 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">자동 승인</p>
|
||||
<p className="text-sm text-text-secondary mt-0.5">
|
||||
신규 가입자를 자동으로 승인합니다.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleToggleAutoApprove}
|
||||
disabled={saving}
|
||||
className={`relative w-11 h-6 rounded-full transition-colors cursor-pointer disabled:opacity-50 ${
|
||||
isAutoApprove ? 'bg-accent' : 'bg-bg-tertiary'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
|
||||
isAutoApprove ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr className="my-5 border-border-default" />
|
||||
|
||||
{/* 기본 부여 롤 */}
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">기본 부여 롤</p>
|
||||
<p className="text-sm text-text-secondary mt-0.5 mb-4">
|
||||
자동 승인 시 신규 사용자에게 배정되는 롤입니다.
|
||||
</p>
|
||||
|
||||
{roles.length === 0 ? (
|
||||
<p className="text-sm text-text-muted">등록된 롤이 없습니다.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{roles.map((role) => (
|
||||
<label
|
||||
key={role.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border border-border-default hover:bg-bg-secondary transition cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={role.defaultGrant}
|
||||
onChange={() => handleToggleDefaultGrant(role.id, role.defaultGrant)}
|
||||
disabled={saving}
|
||||
className="w-4 h-4 accent-accent cursor-pointer"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-medium text-text-primary text-sm">{role.name}</span>
|
||||
<span className="text-text-secondary text-sm ml-2">{role.description}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 안내 / 경고 */}
|
||||
<div className="mt-4 space-y-3">
|
||||
{isAutoApprove && (
|
||||
<Alert type="info">
|
||||
<code>@gcsc.co.kr</code> 도메인 사용자가 가입 즉시 <strong>ACTIVE</strong> 상태가
|
||||
되며, 기본 부여 롤이 자동 배정됩니다.
|
||||
</Alert>
|
||||
)}
|
||||
{isAutoApprove && defaultGrantCount === 0 && (
|
||||
<Alert type="warning">
|
||||
자동 승인이 활성화되어 있지만 기본 부여 롤이 없습니다. 신규 사용자에게 롤이 배정되지
|
||||
않아 접근 가능한 페이지가 제한될 수 있습니다.
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,7 @@ export interface Role {
|
||||
name: string;
|
||||
description: string;
|
||||
urlPatterns: string[];
|
||||
defaultGrant: boolean;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
@ -73,3 +74,8 @@ export interface LoginHistory {
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
export interface RegistrationSettings {
|
||||
autoApprove: boolean;
|
||||
defaultRoles: Role[];
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export const ADMIN_NAV: NavItem[] = [
|
||||
{ path: '/admin/users', label: '사용자 관리' },
|
||||
{ path: '/admin/roles', label: '롤 관리' },
|
||||
{ path: '/admin/permissions', label: '권한 관리' },
|
||||
{ path: '/admin/settings', label: '설정' },
|
||||
{ path: '/admin/stats', label: '통계' },
|
||||
];
|
||||
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user