import { useEffect, useState, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { Shield, Users, UserCheck, Key, Lock, FileText, Loader2, RefreshCw, UserCog, } from 'lucide-react'; import { fetchUsers, fetchUserStats, fetchAuditLogs, fetchAuditStats, unlockUser, type AdminUser, type UserStats, type AuditLog as ApiAuditLog, type AuditStats, } from '@/services/adminApi'; import { formatDateTime } from '@shared/utils/dateFormat'; import { getRoleBadgeStyle } from '@shared/constants/userRoles'; import { getUserAccountStatusIntent, getUserAccountStatusLabel } from '@shared/constants/userAccountStatuses'; import { useSettingsStore } from '@stores/settingsStore'; import { PermissionsPanel } from './PermissionsPanel'; import { UserRoleAssignDialog } from './UserRoleAssignDialog'; /* * SFR-01: 역할 기반 권한 관리(RBAC) - 백엔드 연동 버전 * * 4개 탭: * 1) 역할 관리 - GET /api/roles (admin:role-management) + 사용자 통계 * 2) 사용자 관리 - GET /api/admin/users + 잠금 해제 * 3) 감사 로그 - GET /api/admin/audit-logs + GET /api/admin/stats/audit * 4) 보안 정책 - 정적 정보 */ type Tab = 'roles' | 'users' | 'audit' | 'policy'; export function AccessControl() { const { t } = useTranslation('admin'); const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const [tab, setTab] = useState('roles'); // 공통 상태 const [error, setError] = useState(''); // 사용자 목록 const [users, setUsers] = useState([]); const [userStats, setUserStats] = useState(null); const [usersLoading, setUsersLoading] = useState(false); // 감사 로그 const [auditLogs, setAuditLogs] = useState([]); const [auditStats, setAuditStats] = useState(null); const [auditLoading, setAuditLoading] = useState(false); // 역할 배정 다이얼로그 const [assignTarget, setAssignTarget] = useState(null); // 사용자 + 통계 로드 const loadUsers = useCallback(async () => { setUsersLoading(true); setError(''); try { const [u, s] = await Promise.all([fetchUsers(), fetchUserStats()]); setUsers(u); setUserStats(s); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setUsersLoading(false); } }, []); const loadAudit = useCallback(async () => { setAuditLoading(true); setError(''); try { const [logs, stats] = await Promise.all([fetchAuditLogs(0, 100), fetchAuditStats()]); setAuditLogs(logs.content); setAuditStats(stats); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setAuditLoading(false); } }, []); // 탭 전환 시 자동 로드 (roles 탭은 PermissionsPanel이 자체 로드) useEffect(() => { if (tab === 'users') loadUsers(); else if (tab === 'audit') loadAudit(); }, [tab, loadUsers, loadAudit]); const handleUnlock = async (userId: string, acnt: string) => { if (!confirm(`계정 ${acnt} 잠금을 해제하시겠습니까?`)) return; try { await unlockUser(userId); await loadUsers(); } catch (e: unknown) { alert('실패: ' + (e instanceof Error ? e.message : 'unknown')); } }; // ── 사용자 테이블 컬럼 ────────────── // eslint-disable-next-line react-hooks/exhaustive-deps const userColumns: DataColumn>[] = useMemo(() => [ { key: 'userAcnt', label: '계정', width: '90px', render: (v) => {v as string} }, { key: 'userNm', label: '이름', width: '80px', sortable: true, render: (v) => {v as string} }, { key: 'rnkpNm', label: '직급', width: '60px', render: (v) => {(v as string) || '-'} }, { key: 'email', label: '이메일', render: (v) => {(v as string) || '-'} }, { key: 'roles', label: '역할', width: '120px', render: (v) => { const list = (v as string[]) || []; return (
{list.map((r) => ( {r} ))}
); }, }, { key: 'userSttsCd', label: '상태', width: '70px', sortable: true, render: (v) => { const s = v as string; return {getUserAccountStatusLabel(s, tc, lang)}; }, }, { key: 'failCnt', label: '실패', width: '50px', align: 'center', render: (v) => 0 ? 'text-red-400' : 'text-hint'}`}>{v as number} }, { key: 'authProvider', label: '인증', width: '70px', render: (v) => {v as string} }, { key: 'lastLoginDtm', label: '최종 로그인', width: '140px', sortable: true, render: (v) => ( {formatDateTime(v as string)} ), }, { key: 'userId', label: '관리', width: '90px', align: 'center', sortable: false, render: (_v, row) => (
{row.userSttsCd === 'LOCKED' && ( )}
), }, ], []); // ── 감사 로그 컬럼 ────────────── const auditColumns: DataColumn>[] = useMemo(() => [ { key: 'createdAt', label: '일시', width: '160px', sortable: true, render: (v) => {formatDateTime(v as string)} }, { key: 'userAcnt', label: '사용자', width: '90px', sortable: true, render: (v) => {(v as string) || '-'} }, { key: 'actionCd', label: '액션', width: '180px', sortable: true, render: (v) => {v as string} }, { key: 'resourceType', label: '리소스', width: '110px', render: (v) => {(v as string) || '-'} }, { key: 'ipAddress', label: 'IP', width: '120px', render: (v) => {(v as string) || '-'} }, { key: 'result', label: '결과', width: '70px', sortable: true, render: (v) => { const r = v as string; const c = r === 'SUCCESS' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'; return {r || '-'}; }, }, { key: 'failReason', label: '실패 사유', render: (v) => {(v as string) || '-'} }, ], []); return (

{t('accessControl.title')}

{t('accessControl.desc')}

{userStats && (
활성 {userStats.active}| 잠금 {userStats.locked} |{userStats.total}
)}
{/* 탭 */}
{([ { key: 'roles', icon: Shield, label: '역할 관리' }, { key: 'users', icon: Users, label: '사용자 관리' }, { key: 'audit', icon: FileText, label: '감사 로그' }, { key: 'policy', icon: Lock, label: '보안 정책' }, ] as const).map((tt) => ( ))}
{error &&
에러: {error}
} {/* ── 역할 관리 (PermissionsPanel: 트리 + R/C/U/D 매트릭스) ── */} {tab === 'roles' && } {/* ── 사용자 관리 ── */} {tab === 'users' && ( <> {/* 통계 카드 */} {userStats && (
)} {usersLoading &&
} {!usersLoading && ( )[]} columns={userColumns} pageSize={10} searchPlaceholder="계정, 이름, 이메일 검색..." searchKeys={['userAcnt', 'userNm', 'email', 'rnkpNm']} exportFilename="사용자목록" exportResource="admin:user-management" showPagination /> )} )} {/* ── 감사 로그 ── */} {tab === 'audit' && ( <> {/* 통계 카드 */} {auditStats && (
)} {/* 액션별 분포 */} {auditStats && auditStats.byAction.length > 0 && ( 액션별 분포 (7일)
{auditStats.byAction.map((a) => ( {a.action} {a.count} ))}
)} {auditLoading &&
} {!auditLoading && ( )[]} columns={auditColumns} pageSize={20} searchPlaceholder="사용자, 액션, IP 검색..." searchKeys={['userAcnt', 'actionCd', 'resourceType', 'ipAddress']} exportFilename="감사로그" exportResource="admin:audit-logs" title="모든 운영자 의사결정 자동 기록 (audit_log)" showPagination /> )} )} {/* 역할 배정 다이얼로그 */} {assignTarget && ( setAssignTarget(null)} onSaved={loadUsers} /> )} {/* ── 보안 정책 ── */} {tab === 'policy' && (
)}
); } function StatCard({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value.toLocaleString()}
); } function PolicyCard({ title, rows }: { title: string; rows: [string, string][] }) { return ( {title} {rows.map(([k, v]) => (
{k} {v}
))}
); }