Merge pull request 'feat: 관리자 가입 설정 페이지 추가' (#8) from develop into main
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 15s

Reviewed-on: #8
This commit is contained in:
htlee 2026-02-16 23:35:26 +09:00
커밋 0ed69e4f65
6개의 변경된 파일187개의 추가작업 그리고 3개의 파일을 삭제

파일 보기

@ -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)}

파일 보기

@ -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: '통계' },
];