merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 11:20:20 +09:00
2 changed files with 46 additions and 13 deletions
+39 -7
View File
@@ -3,6 +3,7 @@ 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';
@@ -56,12 +57,15 @@ export function UserSessionsPage() {
{
key: 'user',
header: '사용자',
className: 'w-[240px] px-5 py-3 font-medium',
className: 'w-[280px] px-5 py-3 font-medium',
cell: (row) => (
<div className="flex min-w-0 items-center gap-3">
<UserAvatar session={row} />
<div className="min-w-0">
<p className="truncate font-semibold">{row.user}</p>
{row.email ? <p className="mt-1 truncate text-xs font-medium text-muted-foreground">{row.email}</p> : null}
</div>
</div>
)
},
{ key: 'session', header: '세션', className: 'w-28 px-5 py-3', cell: (row) => <span className="font-mono text-xs text-muted-foreground">{row.session}</span> },
@@ -174,13 +178,16 @@ export function UserSessionsPage() {
{detailSession ? (
<div className="space-y-5">
<div className="flex min-w-0 items-start justify-between gap-4 rounded-md border border-border bg-muted/20 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<UserAvatar session={detailSession} size="lg" />
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-foreground">{detailSession.user}</p>
<p className="mt-1 truncate text-xs text-muted-foreground">{detailSession.email ?? '-'}</p>
</div>
</div>
<StatusBadge status={detailSession.state} />
</div>
<div className="grid gap-3 text-sm sm:grid-cols-2">
<div className="grid gap-3 text-sm">
<DetailItem label="사용자 ID" value={detailSession.userId} mono />
<DetailItem label="조직" value={detailSession.primaryAffiliation?.organizationName ?? '-'} />
<DetailItem label="조직 유형" value={organizationTypeLabel(detailSession.primaryAffiliation?.organizationType)} />
@@ -212,11 +219,36 @@ function organizationTypeLabel(type?: string | null) {
return type ? labels[type] ?? type : '-';
}
function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
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 (
<div className="min-w-0 rounded-md border border-border bg-background px-3 py-2">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<p className={`mt-1 truncate text-sm text-foreground ${mono ? 'font-mono text-xs' : 'font-medium'}`}>{value || '-'}</p>
</div>
<img
src={session.avatarUrl}
alt=""
className={cn(sizeClassName, 'shrink-0 rounded-full border border-border bg-muted object-cover')}
onError={() => setFailed(true)}
/>
);
}
return (
<span className={cn(sizeClassName, 'inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-muted font-semibold text-muted-foreground')}>
{initial}
</span>
);
}
function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
return (
<label className="block min-w-0">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
<div className={cn('mt-2 min-h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-medium text-foreground', mono && 'break-all font-mono text-xs font-normal')}>
{value || '-'}
</div>
</label>
);
}
+1
View File
@@ -39,6 +39,7 @@ export type UserSession = {
sessionId?: string | null;
user: string;
email?: string;
avatarUrl?: string | null;
role: string;
session: string;
expiresAt?: string | null;