feat: 관리자 가입 설정 페이지 추가 #8
@ -13,6 +13,7 @@ import { UserManagement } from './pages/admin/UserManagement';
|
|||||||
import { RoleManagement } from './pages/admin/RoleManagement';
|
import { RoleManagement } from './pages/admin/RoleManagement';
|
||||||
import { PermissionManagement } from './pages/admin/PermissionManagement';
|
import { PermissionManagement } from './pages/admin/PermissionManagement';
|
||||||
import { StatsPage } from './pages/admin/StatsPage';
|
import { StatsPage } from './pages/admin/StatsPage';
|
||||||
|
import { SettingsPage } from './pages/admin/SettingsPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -36,6 +37,7 @@ function App() {
|
|||||||
<Route path="/admin/users" element={<UserManagement />} />
|
<Route path="/admin/users" element={<UserManagement />} />
|
||||||
<Route path="/admin/roles" element={<RoleManagement />} />
|
<Route path="/admin/roles" element={<RoleManagement />} />
|
||||||
<Route path="/admin/permissions" element={<PermissionManagement />} />
|
<Route path="/admin/permissions" element={<PermissionManagement />} />
|
||||||
|
<Route path="/admin/settings" element={<SettingsPage />} />
|
||||||
<Route path="/admin/stats" element={<StatsPage />} />
|
<Route path="/admin/stats" element={<StatsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -12,11 +12,11 @@ import { AuthContext } from './AuthContext';
|
|||||||
const DEV_MOCK_USER: User = {
|
const DEV_MOCK_USER: User = {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'htlee@gcsc.co.kr',
|
email: 'htlee@gcsc.co.kr',
|
||||||
name: '이현태 (DEV)',
|
name: '김개발 (DEV)',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
roles: [{ id: 1, name: 'ADMIN', description: '관리자', urlPatterns: ['/**'] }],
|
roles: [{ id: 1, name: 'ADMIN', description: '관리자', urlPatterns: ['/**'], defaultGrant: false }],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastLoginAt: new Date().toISOString(),
|
lastLoginAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -93,7 +93,14 @@ export function RoleManagement() {
|
|||||||
roles.map((role) => (
|
roles.map((role) => (
|
||||||
<div key={role.id} className="bg-surface border border-border-default rounded-xl p-5">
|
<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-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">
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => openEdit(role)}
|
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;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
urlPatterns: string[];
|
urlPatterns: string[];
|
||||||
|
defaultGrant: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
@ -73,3 +74,8 @@ export interface LoginHistory {
|
|||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
userAgent: 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/users', label: '사용자 관리' },
|
||||||
{ path: '/admin/roles', label: '롤 관리' },
|
{ path: '/admin/roles', label: '롤 관리' },
|
||||||
{ path: '/admin/permissions', label: '권한 관리' },
|
{ path: '/admin/permissions', label: '권한 관리' },
|
||||||
|
{ path: '/admin/settings', label: '설정' },
|
||||||
{ path: '/admin/stats', label: '통계' },
|
{ path: '/admin/stats', label: '통계' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user