import { useState, useEffect, useCallback, useRef } from 'react'; import { fetchUsers, fetchRoles, fetchOrgs, createUserApi, updateUserApi, changePasswordApi, approveUserApi, rejectUserApi, assignRolesApi, type UserListItem, type RoleWithPermissions, type OrgItem, } from '@common/services/authApi'; import { getRoleColor, statusLabels } from './adminConstants'; const PAGE_SIZE = 15; // ─── 포맷 헬퍼 ───────────────────────────────────────────────── function formatDate(dateStr: string | null) { if (!dateStr) return '-'; return new Date(dateStr).toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } // ─── 사용자 등록 모달 ─────────────────────────────────────────── interface RegisterModalProps { allRoles: RoleWithPermissions[]; allOrgs: OrgItem[]; onClose: () => void; onSuccess: () => void; } function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalProps) { const [account, setAccount] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [rank, setRank] = useState(''); const [orgSn, setOrgSn] = useState(() => { const defaultOrg = allOrgs.find((o) => o.orgNm === '기동방제과'); return defaultOrg ? defaultOrg.orgSn : ''; }); const [email, setEmail] = useState(''); const [roleSns, setRoleSns] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const toggleRole = (sn: number) => { setRoleSns((prev) => (prev.includes(sn) ? prev.filter((s) => s !== sn) : [...prev, sn])); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!account.trim() || !password.trim() || !name.trim()) { setError('계정, 비밀번호, 사용자명은 필수 항목입니다.'); return; } setSubmitting(true); setError(null); try { await createUserApi({ account: account.trim(), password, name: name.trim(), rank: rank.trim() || undefined, orgSn: orgSn !== '' ? orgSn : undefined, roleSns: roleSns.length > 0 ? roleSns : undefined, }); onSuccess(); onClose(); } catch (err) { setError('사용자 등록에 실패했습니다.'); console.error('사용자 등록 실패:', err); } finally { setSubmitting(false); } }; return (
{/* 헤더 */}

사용자 등록

{/* 폼 */}
{/* 계정 */}
setAccount(e.target.value)} placeholder="로그인 계정 ID" className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono" />
{/* 비밀번호 */}
setPassword(e.target.value)} placeholder="초기 비밀번호" className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono" />
{/* 사용자명 */}
setName(e.target.value)} placeholder="실명" className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean" />
{/* 직급 */}
setRank(e.target.value)} placeholder="예: 팀장, 주임 등" className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean" />
{/* 소속 */}
{/* 이메일 */}
setEmail(e.target.value)} placeholder="이메일 주소" className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono" />
{/* 역할 */}
{allRoles.length === 0 ? (

역할 없음

) : ( allRoles.map((role, idx) => { const color = getRoleColor(role.code, idx); return ( ); }) )}
{/* 에러 메시지 */} {error &&

{error}

}
{/* 푸터 */}
); } // ─── 사용자 상세/수정 모달 ──────────────────────────────────── interface UserDetailModalProps { user: UserListItem; allRoles: RoleWithPermissions[]; allOrgs: OrgItem[]; onClose: () => void; onUpdated: () => void; } function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalProps) { const [name, setName] = useState(user.name); const [rank, setRank] = useState(user.rank || ''); const [orgSn, setOrgSn] = useState(user.orgSn ?? ''); const [saving, setSaving] = useState(false); const [newPassword, setNewPassword] = useState(''); const [resetPwLoading, setResetPwLoading] = useState(false); const [resetPwDone, setResetPwDone] = useState(false); const [unlockLoading, setUnlockLoading] = useState(false); const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null); const handleSaveInfo = async () => { setSaving(true); setMessage(null); try { await updateUserApi(user.id, { name: name.trim(), rank: rank.trim() || undefined, orgSn: orgSn !== '' ? orgSn : null, }); setMessage({ text: '사용자 정보가 수정되었습니다.', type: 'success' }); onUpdated(); } catch { setMessage({ text: '사용자 정보 수정에 실패했습니다.', type: 'error' }); } finally { setSaving(false); } }; const handleResetPassword = async () => { if (!newPassword.trim()) { setMessage({ text: '새 비밀번호를 입력하세요.', type: 'error' }); return; } setResetPwLoading(true); setMessage(null); try { await changePasswordApi(user.id, newPassword); setMessage({ text: '비밀번호가 초기화되었습니다.', type: 'success' }); setResetPwDone(true); setNewPassword(''); } catch { setMessage({ text: '비밀번호 초기화에 실패했습니다.', type: 'error' }); } finally { setResetPwLoading(false); } }; const handleUnlock = async () => { setUnlockLoading(true); setMessage(null); try { await updateUserApi(user.id, { status: 'ACTIVE' }); setMessage({ text: '계정 잠금이 해제되었습니다.', type: 'success' }); onUpdated(); } catch { setMessage({ text: '잠금 해제에 실패했습니다.', type: 'error' }); } finally { setUnlockLoading(false); } }; return (
{/* 헤더 */}

사용자 정보

{user.account}

{/* 기본 정보 수정 */}

기본 정보 수정

setName(e.target.value)} className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean" />
setRank(e.target.value)} placeholder="예: 팀장" className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean" />
{/* 구분선 */}
{/* 비밀번호 초기화 */}

비밀번호 초기화

setNewPassword(e.target.value)} placeholder="새 비밀번호 입력" className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono" />

초기화 후 사용자에게 새 비밀번호를 전달하세요.

{/* 구분선 */}
{/* 계정 잠금 해제 */}

계정 상태

{(statusLabels[user.status] || statusLabels.INACTIVE).label} {user.failCount > 0 && ( (로그인 실패 {user.failCount}회) )}
{user.status === 'LOCKED' && (

비밀번호 5회 이상 오류로 잠금 처리됨

)}
{user.status === 'LOCKED' && ( )}
{/* 기타 정보 (읽기 전용) */}

기타 정보

이메일: {user.email || '-'}
OAuth: {user.oauthProvider || '-'}
최종 로그인: {user.lastLogin ? formatDate(user.lastLogin) : '-'}
등록일: {formatDate(user.regDtm)}
{/* 메시지 */} {message && (
{message.text}
)}
{/* 푸터 */}
); } // ─── 사용자 관리 패널 ───────────────────────────────────────── function UsersPanel() { const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [orgFilter, setOrgFilter] = useState(''); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [allRoles, setAllRoles] = useState([]); const [allOrgs, setAllOrgs] = useState([]); const [roleEditUserId, setRoleEditUserId] = useState(null); const [selectedRoleSns, setSelectedRoleSns] = useState([]); const [showRegisterModal, setShowRegisterModal] = useState(false); const [detailUser, setDetailUser] = useState(null); const [currentPage, setCurrentPage] = useState(1); const roleDropdownRef = useRef(null); const loadUsers = useCallback(async () => { setLoading(true); try { const data = await fetchUsers(searchTerm || undefined, statusFilter || undefined); setUsers(data); setCurrentPage(1); } catch (err) { console.error('사용자 목록 조회 실패:', err); } finally { setLoading(false); } }, [searchTerm, statusFilter]); useEffect(() => { loadUsers(); }, [loadUsers]); useEffect(() => { fetchRoles().then(setAllRoles).catch(console.error); fetchOrgs().then(setAllOrgs).catch(console.error); }, []); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (roleDropdownRef.current && !roleDropdownRef.current.contains(e.target as Node)) { setRoleEditUserId(null); } }; if (roleEditUserId) { document.addEventListener('mousedown', handleClickOutside); } return () => document.removeEventListener('mousedown', handleClickOutside); }, [roleEditUserId]); // ─── 필터링 (org 클라이언트 사이드) ─────────────────────────── const filteredUsers = orgFilter ? users.filter((u) => String(u.orgSn) === orgFilter) : users; // ─── 페이지네이션 ────────────────────────────────────────────── const totalCount = filteredUsers.length; const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)); const pagedUsers = filteredUsers.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); // ─── 액션 핸들러 ────────────────────────────────────────────── const handleUnlock = async (userId: string) => { try { await updateUserApi(userId, { status: 'ACTIVE' }); await loadUsers(); } catch (err) { console.error('계정 잠금 해제 실패:', err); } }; const handleApprove = async (userId: string) => { try { await approveUserApi(userId); await loadUsers(); } catch (err) { console.error('사용자 승인 실패:', err); } }; const handleReject = async (userId: string) => { try { await rejectUserApi(userId); await loadUsers(); } catch (err) { console.error('사용자 거절 실패:', err); } }; const handleDeactivate = async (userId: string) => { try { await updateUserApi(userId, { status: 'INACTIVE' }); await loadUsers(); } catch (err) { console.error('사용자 비활성화 실패:', err); } }; const handleActivate = async (userId: string) => { try { await updateUserApi(userId, { status: 'ACTIVE' }); await loadUsers(); } catch (err) { console.error('사용자 활성화 실패:', err); } }; const handleOpenRoleEdit = (user: UserListItem) => { setRoleEditUserId(user.id); setSelectedRoleSns(user.roleSns || []); }; const toggleRoleSelection = (roleSn: number) => { setSelectedRoleSns((prev) => prev.includes(roleSn) ? prev.filter((s) => s !== roleSn) : [...prev, roleSn], ); }; const handleSaveRoles = async (userId: string) => { try { await assignRolesApi(userId, selectedRoleSns); await loadUsers(); setRoleEditUserId(null); } catch (err) { console.error('역할 할당 실패:', err); } }; const pendingCount = users.filter((u) => u.status === 'PENDING').length; return ( <>
{/* 헤더 */}

사용자 관리

총 {filteredUsers.length}명

{pendingCount > 0 && ( 승인대기 {pendingCount}명 )}
{/* 소속 필터 */} {/* 상태 필터 */} {/* 텍스트 검색 */} setSearchTerm(e.target.value)} className="w-56 px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean" />
{/* 테이블 */}
{loading ? (
불러오는 중...
) : ( {pagedUsers.length === 0 ? ( ) : ( pagedUsers.map((user, idx) => { const statusInfo = statusLabels[user.status] || statusLabels.INACTIVE; const rowNum = (currentPage - 1) * PAGE_SIZE + idx + 1; return ( {/* 번호 */} {/* ID(account) */} {/* 사용자명 */} {/* 직급 */} {/* 소속 */} {/* 이메일 */} {/* 역할 (인라인 편집) */} {/* 승인상태 */} {/* 관리 */} ); }) )}
번호 ID 사용자명 직급 소속 이메일 역할 승인상태 관리
조회된 사용자가 없습니다.
{rowNum} {user.account} {user.rank || '-'} {user.orgAbbr || user.orgName || '-'} {user.email || '-'}
handleOpenRoleEdit(user)} title="클릭하여 역할 변경" > {user.roles.length > 0 ? ( user.roles.map((roleCode, roleIdx) => { const color = getRoleColor(roleCode, roleIdx); const roleName = allRoles.find((r) => r.code === roleCode)?.name || roleCode; return ( {roleName} ); }) ) : ( 역할 없음 )}
{roleEditUserId === user.id && (
역할 선택
{allRoles.map((role, roleIdx) => { const color = getRoleColor(role.code, roleIdx); return ( ); })}
)}
{statusInfo.label}
{user.status === 'PENDING' && ( <> )} {user.status === 'LOCKED' && ( )} {user.status === 'ACTIVE' && ( )} {(user.status === 'INACTIVE' || user.status === 'REJECTED') && ( )}
)}
{/* 페이지네이션 */} {!loading && totalPages > 1 && (
{(currentPage - 1) * PAGE_SIZE + 1}–{Math.min(currentPage * PAGE_SIZE, totalCount)} /{' '} {totalCount}명
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter((p) => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2) .reduce<(number | '...')[]>((acc, p, i, arr) => { if ( i > 0 && typeof arr[i - 1] === 'number' && (p as number) - (arr[i - 1] as number) > 1 ) { acc.push('...'); } acc.push(p); return acc; }, []) .map((item, i) => item === '...' ? ( ) : ( ), )}
)}
{/* 사용자 등록 모달 */} {showRegisterModal && ( setShowRegisterModal(false)} onSuccess={loadUsers} /> )} {/* 사용자 상세/수정 모달 */} {detailUser && ( setDetailUser(null)} onUpdated={() => { loadUsers(); // 최신 정보로 모달 갱신을 위해 닫지 않음 }} /> )} ); } export default UsersPanel;