1
0
forked from baron/baron-sso

i18n refresh and frontend fixes

This commit is contained in:
Lectom C Han
2026-02-10 19:15:51 +09:00
parent 2441c64598
commit b6d3b69cda
44 changed files with 8603 additions and 1760 deletions

View File

@@ -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>