forked from baron/baron-sso
i18n refresh and frontend fixes
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { deleteUser, fetchUsers } from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
function UserListPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -67,7 +68,12 @@ function UserListPage() {
|
||||
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
|
||||
?.data?.error;
|
||||
const fallbackError =
|
||||
!errorMsg && query.isError ? "사용자 목록 조회에 실패했습니다." : null;
|
||||
!errorMsg && query.isError
|
||||
? t(
|
||||
"msg.admin.users.list.fetch_error",
|
||||
"사용자 목록 조회에 실패했습니다.",
|
||||
)
|
||||
: null;
|
||||
|
||||
const items = query.data?.items ?? [];
|
||||
const total = query.data?.total ?? 0;
|
||||
@@ -80,7 +86,15 @@ function UserListPage() {
|
||||
}, [items]);
|
||||
|
||||
const handleDelete = (userId: string, userName: string) => {
|
||||
if (!window.confirm(`사용자 "${userName}"을(를) 정말 삭제하시겠습니까?`)) {
|
||||
if (
|
||||
!window.confirm(
|
||||
t(
|
||||
"msg.admin.users.list.delete_confirm",
|
||||
'사용자 "{{name}}"을(를) 정말 삭제하시겠습니까?',
|
||||
{ name: userName },
|
||||
),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate(userId);
|
||||
@@ -91,13 +105,20 @@ function UserListPage() {
|
||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>Users</span>
|
||||
<span>{t("ui.admin.users.list.breadcrumb.section", "Users")}</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">List</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.users.list.breadcrumb.list", "List")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">사용자 관리</h2>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.users.list.title", "사용자 관리")}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--color-muted)]">
|
||||
시스템 사용자를 조회하고 관리합니다. (Local DB)
|
||||
{t(
|
||||
"msg.admin.users.list.subtitle",
|
||||
"시스템 사용자를 조회하고 관리합니다. (Local DB)",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -107,12 +128,12 @@ function UserListPage() {
|
||||
disabled={query.isFetching}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
새로고침
|
||||
{t("ui.common.refresh", "새로고침")}
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link to="/users/new">
|
||||
<Plus size={16} />
|
||||
사용자 추가
|
||||
{t("ui.admin.users.list.add", "사용자 추가")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -121,9 +142,15 @@ function UserListPage() {
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>User Registry</CardTitle>
|
||||
<CardTitle>
|
||||
{t("ui.admin.users.list.registry.title", "User Registry")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
총 {total}명의 사용자가 등록되어 있습니다.
|
||||
{t(
|
||||
"msg.admin.users.list.registry.count",
|
||||
"총 {{count}}명의 사용자가 등록되어 있습니다.",
|
||||
{ count: total },
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -132,7 +159,10 @@ function UserListPage() {
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="이름 또는 이메일 검색..."
|
||||
placeholder={t(
|
||||
"ui.admin.users.list.search_placeholder",
|
||||
"이름 또는 이메일 검색...",
|
||||
)}
|
||||
className="pl-9"
|
||||
value={searchDraft}
|
||||
onChange={(e) => setSearchDraft(e.target.value)}
|
||||
@@ -140,7 +170,7 @@ function UserListPage() {
|
||||
/>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={handleSearch}>
|
||||
검색
|
||||
{t("ui.common.search", "검색")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -154,26 +184,41 @@ function UserListPage() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NAME / EMAIL</TableHead>
|
||||
<TableHead>ROLE</TableHead>
|
||||
<TableHead>STATUS</TableHead>
|
||||
<TableHead>TENANT / DEPT</TableHead>
|
||||
<TableHead>CREATED</TableHead>
|
||||
<TableHead className="text-right">ACTIONS</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.users.list.table.role", "ROLE")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.users.list.table.status", "STATUS")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t(
|
||||
"ui.admin.users.list.table.tenant_dept",
|
||||
"TENANT / DEPT",
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.users.list.table.created", "CREATED")}
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{t("ui.admin.users.list.table.actions", "ACTIONS")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{query.isLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-24 text-center">
|
||||
로딩 중...
|
||||
{t("msg.common.loading", "로딩 중...")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!query.isLoading && items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-24 text-center">
|
||||
검색 결과가 없습니다.
|
||||
{t("msg.admin.users.list.empty", "검색 결과가 없습니다.")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -193,7 +238,9 @@ function UserListPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{user.role}</Badge>
|
||||
<Badge variant="outline">
|
||||
{t(`ui.common.role.${user.role}`, user.role)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
@@ -201,7 +248,7 @@ function UserListPage() {
|
||||
user.status === "active" ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{user.status}
|
||||
{t(`ui.common.status.${user.status}`, user.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -211,7 +258,13 @@ function UserListPage() {
|
||||
</span>
|
||||
{user.tenant && (
|
||||
<span className="text-[10px] text-muted-foreground uppercase">
|
||||
Slug: {user.tenant.slug}
|
||||
{t(
|
||||
"ui.admin.users.list.tenant_slug",
|
||||
"Slug: {{slug}}",
|
||||
{
|
||||
slug: user.tenant.slug,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -228,7 +281,11 @@ function UserListPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(`/users/${user.id}`)}
|
||||
aria-label={`사용자 수정: ${user.name}`}
|
||||
aria-label={t(
|
||||
"ui.admin.users.list.edit_aria",
|
||||
"사용자 수정: {{name}}",
|
||||
{ name: user.name },
|
||||
)}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</Button>
|
||||
@@ -238,7 +295,11 @@ function UserListPage() {
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => handleDelete(user.id, user.name)}
|
||||
disabled={deleteMutation.isPending}
|
||||
aria-label={`사용자 삭제: ${user.name}`}
|
||||
aria-label={t(
|
||||
"ui.admin.users.list.delete_aria",
|
||||
"사용자 삭제: {{name}}",
|
||||
{ name: user.name },
|
||||
)}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
@@ -260,10 +321,13 @@ function UserListPage() {
|
||||
disabled={page === 1 || query.isFetching}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Previous
|
||||
{t("ui.common.previous", "Previous")}
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Page {page} of {totalPages}
|
||||
{t("ui.common.page_of", "Page {{page}} of {{total}}", {
|
||||
page,
|
||||
total: totalPages,
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -271,7 +335,7 @@ function UserListPage() {
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages || query.isFetching}
|
||||
>
|
||||
Next
|
||||
{t("ui.common.next", "Next")}
|
||||
<ChevronRight size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user