import { useState, useEffect, useCallback } from 'react'; import { bypassAccountApi, type BypassAccountResponse, type BypassAccountUpdateRequest, type PageResponse, } from '../api/bypassAccountApi'; import { useToastContext } from '../contexts/ToastContext'; import Pagination from '../components/Pagination'; import ConfirmModal from '../components/ConfirmModal'; import LoadingSpinner from '../components/LoadingSpinner'; const STATUS_TABS = [ { value: '', label: '전체' }, { value: 'ACTIVE', label: 'ACTIVE' }, { value: 'SUSPENDED', label: 'SUSPENDED' }, { value: 'EXPIRED', label: 'EXPIRED' }, ] as const; const STATUS_BADGE_COLORS: Record = { ACTIVE: 'bg-emerald-100 text-emerald-700', SUSPENDED: 'bg-amber-100 text-amber-700', EXPIRED: 'bg-red-100 text-red-700', }; const ACCOUNT_STATUS_OPTIONS = ['ACTIVE', 'SUSPENDED', 'EXPIRED']; const PAGE_SIZE = 20; interface EditFormState { displayName: string; organization: string; email: string; phone: string; status: string; accessStartDate: string; accessEndDate: string; } export default function BypassAccountManagement() { const { showToast } = useToastContext(); const [pageData, setPageData] = useState | null>(null); const [loading, setLoading] = useState(true); const [statusFilter, setStatusFilter] = useState(''); const [page, setPage] = useState(0); // Edit modal const [editTarget, setEditTarget] = useState(null); const [editForm, setEditForm] = useState({ displayName: '', organization: '', email: '', phone: '', status: '', accessStartDate: '', accessEndDate: '', }); const [editSubmitting, setEditSubmitting] = useState(false); // Delete confirm const [deleteTarget, setDeleteTarget] = useState(null); const [deleteSubmitting, setDeleteSubmitting] = useState(false); // Password reset confirm + credential modal const [resetTarget, setResetTarget] = useState(null); const [resetSubmitting, setResetSubmitting] = useState(false); const [credentialAccount, setCredentialAccount] = useState(null); const loadAccounts = useCallback(async () => { setLoading(true); try { const res = await bypassAccountApi.getAccounts(statusFilter || undefined, page, PAGE_SIZE); setPageData(res.data ?? null); } catch (err) { showToast('계정 목록 조회 실패', 'error'); console.error(err); } finally { setLoading(false); } }, [showToast, statusFilter, page]); useEffect(() => { loadAccounts(); }, [loadAccounts]); const handleStatusFilterChange = (value: string) => { setStatusFilter(value); setPage(0); }; const openEditModal = (account: BypassAccountResponse) => { setEditTarget(account); setEditForm({ displayName: account.displayName ?? '', organization: account.organization ?? '', email: account.email ?? '', phone: account.phone ?? '', status: account.status, accessStartDate: account.accessStartDate ?? '', accessEndDate: account.accessEndDate ?? '', }); }; const handleEditSubmit = async () => { if (!editTarget) return; setEditSubmitting(true); try { const updateData: BypassAccountUpdateRequest = { status: editForm.status || undefined, accessStartDate: editForm.accessStartDate || undefined, accessEndDate: editForm.accessEndDate || undefined, }; await bypassAccountApi.updateAccount(editTarget.id, updateData); setEditTarget(null); showToast('계정 정보가 수정되었습니다.', 'success'); await loadAccounts(); } catch (err) { showToast('계정 수정 실패', 'error'); console.error(err); } finally { setEditSubmitting(false); } }; const handleDeleteConfirm = async () => { if (!deleteTarget) return; setDeleteSubmitting(true); try { await bypassAccountApi.deleteAccount(deleteTarget.id); setDeleteTarget(null); showToast('계정이 삭제되었습니다.', 'success'); await loadAccounts(); } catch (err) { showToast('계정 삭제 실패', 'error'); console.error(err); } finally { setDeleteSubmitting(false); } }; const handleResetPasswordConfirm = async () => { if (!resetTarget) return; setResetSubmitting(true); try { const res = await bypassAccountApi.resetPassword(resetTarget.id); setResetTarget(null); setCredentialAccount(res.data); showToast('비밀번호가 재설정되었습니다.', 'success'); await loadAccounts(); } catch (err) { showToast('비밀번호 재설정 실패', 'error'); console.error(err); } finally { setResetSubmitting(false); } }; if (loading && !pageData) return ; const accounts = pageData?.content ?? []; return (
{/* 헤더 */}

계정 관리

Bypass API 계정을 조회하고 관리합니다.

{/* 상태 필터 탭 */}
{STATUS_TABS.map((tab) => ( ))}
{/* 테이블 */}
{accounts.length === 0 ? ( ) : ( accounts.map((account) => ( )) )}
Username 표시명 기관 이메일 상태 접근 기간 등록일 액션
등록된 계정이 없습니다.
{account.username} {account.displayName} {account.organization ?? '-'} {account.email ?? '-'} {account.status} {account.accessStartDate && account.accessEndDate ? `${account.accessStartDate} ~ ${account.accessEndDate}` : account.accessStartDate ?? '-'} {account.createdAt ? new Date(account.createdAt).toLocaleDateString('ko-KR') : '-'}
{pageData && pageData.totalPages > 1 && (
)}
{/* 수정 모달 */} {editTarget && (
setEditTarget(null)} >
e.stopPropagation()} >

계정 수정

{editTarget.username}

{/* 신청자 정보 (읽기 전용) */}
표시명: {editTarget.displayName}
기관: {editTarget.organization ?? '-'}
이메일: {editTarget.email ?? '-'}
setEditForm((f) => ({ ...f, accessStartDate: e.target.value }))} className="w-full px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-card text-wing-text focus:outline-none focus:ring-2 focus:ring-wing-accent/50" />
setEditForm((f) => ({ ...f, accessEndDate: e.target.value }))} className="w-full px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-card text-wing-text focus:outline-none focus:ring-2 focus:ring-wing-accent/50" />
)} {/* 삭제 확인 모달 */} setDeleteTarget(null)} /> {/* 비밀번호 재설정 확인 모달 */} setResetTarget(null)} /> {/* 계정 발급 완료 모달 */} {credentialAccount && (
setCredentialAccount(null)} >
e.stopPropagation()} >
비밀번호 재설정 완료
이 화면을 닫으면 비밀번호를 다시 확인할 수 없습니다.
사용자명
{credentialAccount.username}
새 비밀번호
{credentialAccount.plainPassword}
)}
); }