import { MoreHorizontal } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table'; import { Button, Modal } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; import { cn } from '../../lib/utils'; import { loadUserSessions, lockUser, terminateUserSession, unlockUser, type UserSession } from './operations-api'; import { formatDateTime, LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils'; export function UserSessionsPage() { const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); const [menuSession, setMenuSession] = useState<{ id: string; x: number; y: number } | null>(null); const [detailSession, setDetailSession] = useState(null); const loadingVisible = useMinimumLoading(loading); const refresh = () => { setLoading(true); loadUserSessions() .then((nextSessions) => { setSessions(nextSessions); setFailed(false); }) .catch(() => { setSessions([]); setFailed(true); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, []); useEffect(() => { if (!menuSession) return; const close = () => setMenuSession(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setMenuSession(null); }; window.addEventListener('click', close); window.addEventListener('keydown', closeOnEscape); return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', closeOnEscape); }; }, [menuSession]); const rows = useMemo(() => sessions.map((session, index) => ({ ...session, no: sessions.length - index })), [sessions]); const columns = useMemo>>( () => [ { key: 'no', header: 'No.', className: 'w-20 px-5 py-3 font-medium', cell: (row) => {row.no} }, { key: 'user', header: '사용자', className: 'w-[280px] px-5 py-3 font-medium', cell: (row) => (

{row.user}

{row.email ?

{row.email}

: null}
) }, { key: 'session', header: '세션', className: 'w-28 px-5 py-3', cell: (row) => {row.session} }, { key: 'expiresAt', header: '만료', className: 'w-40 px-5 py-3', cell: (row) => {formatDateTime(row.expiresAt)} }, { key: 'lastSeen', header: '최근 접속', className: 'w-40 px-5 py-3', cell: (row) => {formatDateTime(row.lastSeen)} }, { key: 'state', header: '상태', className: 'w-24 px-5 py-3', cell: (row) => }, { key: 'actions', header: '', className: 'w-20 px-5 py-3 text-right font-medium', cell: (row) => (
) } ], [menuSession?.id] ); const filters = useMemo>>( () => [ { key: 'state', label: '상태', options: [...new Set(sessions.map((row) => row.state))].map((state) => ({ label: state, value: state })), getValue: (row) => row.state } ], [sessions] ); return ( <> {failed ? (
사용자/세션 데이터를 불러오지 못했습니다.
) : null} `${row.user} ${row.email ?? ''} ${row.session} ${row.state}`} getRowKey={(row) => row.id} pageSize={6} minWidth={900} loading={loading} emptyMessage="표시할 사용자/세션 데이터가 없습니다." /> {menuSession ? (() => { const row = rows.find((item) => item.id === menuSession.id); if (!row) return null; const locked = row.state === '잠김'; return (
event.stopPropagation()} className="fixed z-[70] min-w-32 overflow-hidden rounded-md border border-border bg-card py-1 text-left shadow-xl" style={{ left: menuSession.x, top: menuSession.y, transform: 'translateX(-100%)' }}>
); })() : null} setDetailSession(null)} className="!max-w-2xl" footer={} > {detailSession ? (

{detailSession.user}

{detailSession.email ?? '-'}

) : null}
); } function organizationTypeLabel(type?: string | null) { const labels: Record = { COMPANY: '회사', SCHOOL: '학교', GOVERNMENT: '공공기관', LAB: '연구소', OTHER: '기타' }; return type ? labels[type] ?? type : '-'; } function UserAvatar({ session, size = 'sm' }: { session: UserSession; size?: 'sm' | 'lg' }) { const [failed, setFailed] = useState(false); const initial = (session.user || session.email || '?').trim().charAt(0).toUpperCase() || '?'; const sizeClassName = size === 'lg' ? 'size-14 text-lg' : 'size-9 text-sm'; if (session.avatarUrl && !failed) { return ( setFailed(true)} /> ); } return ( {initial} ); } function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return ( ); }