1
0
forked from baron/baron-sso

tenants 목록 툴바 레이아웃 정리

This commit is contained in:
2026-06-04 13:08:11 +09:00
parent 29038254dd
commit 47d2f15283

View File

@@ -33,6 +33,7 @@ import {
sortItems, sortItems,
toggleSort, toggleSort,
} from "../../../../../common/core/utils"; } from "../../../../../common/core/utils";
import { SearchFilterBar } from "../../../../../common/ui/search-filter-bar";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table"; import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { RoleGuard } from "../../../components/auth/RoleGuard"; import { RoleGuard } from "../../../components/auth/RoleGuard";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
@@ -708,174 +709,187 @@ function TenantListPage() {
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.", "시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
)} )}
actions={ actions={
<> <div className="min-w-0 space-y-2">
<div className="flex items-center gap-2"> <SearchFilterBar
<div className="relative mr-2 w-64"> primary={
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <>
<Input <div className="relative w-48">
placeholder={t( <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
"ui.admin.tenants.list.search_placeholder", <Input
"테넌트 이름, 슬러그, UUID 검색...", placeholder={t(
)} "ui.admin.tenants.list.search_placeholder",
className="h-9 pl-9" "테넌트 이름, 슬러그, UUID 검색...",
value={search} )}
onChange={(e) => setSearch(e.target.value)} className="h-9 pl-9"
onKeyDown={(e) => { value={search}
if (e.key === "Enter") { onChange={(e) => setSearch(e.target.value)}
query.refetch(); onKeyDown={(e) => {
} if (e.key === "Enter") {
}} query.refetch();
/> }
</div> }}
/>
</div>
<div <div
className="flex rounded-md border bg-background p-0.5" className="flex rounded-md border bg-background p-0.5"
data-testid="tenant-view-mode-toggle" data-testid="tenant-view-mode-toggle"
> >
<Button
type="button"
variant={viewMode === "tree" ? "default" : "ghost"}
size="sm"
className="h-8 gap-1.5"
aria-pressed={viewMode === "tree"}
onClick={() => setViewMode("tree")}
data-testid="tenant-view-tree-btn"
>
<Network size={14} />
{t("ui.admin.tenants.view.tree", "트리")}
</Button>
<Button
type="button"
variant={viewMode === "table" ? "default" : "ghost"}
size="sm"
className="h-8 gap-1.5"
aria-pressed={viewMode === "table"}
onClick={() => setViewMode("table")}
data-testid="tenant-view-table-btn"
>
<List size={14} />
{t("ui.admin.tenants.view.table", "평면")}
</Button>
</div>
<Button
type="button"
variant={scopeTenantId ? "default" : "outline"}
size="sm"
className="h-9 gap-2"
onClick={() => setScopePickerOpen(true)}
data-testid="tenant-scope-picker-btn"
>
<Network size={16} />
{selectedScopeTenant
? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
name: selectedScopeTenant.name,
})
: t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
</Button>
{scopeTenantId ? (
<Button
type="button"
variant="ghost"
size="sm"
className="h-9"
onClick={() => setScopeTenantId("")}
data-testid="tenant-scope-clear-btn"
>
{t("ui.common.clear", "초기화")}
</Button>
) : null}
<RoleGuard roles={["super_admin"]}>
<input
ref={fileInputRef}
type="file"
accept=".csv,text/csv"
className="hidden"
data-testid="tenant-import-input"
onChange={handleImportFile}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button <Button
variant="outline" type="button"
data-testid="tenant-data-mgmt-btn" variant={viewMode === "tree" ? "default" : "ghost"}
className="gap-2" size="sm"
className="h-9 gap-1.5"
aria-pressed={viewMode === "tree"}
onClick={() => setViewMode("tree")}
data-testid="tenant-view-tree-btn"
> >
<LayoutDashboard size={16} /> <Network size={14} />
{t("ui.admin.tenants.data_mgmt", "데이터 관리")} {t("ui.admin.tenants.view.tree", "리")}
<ChevronDown size={14} className="opacity-50" />
</Button> </Button>
</DropdownMenuTrigger> <Button
<DropdownMenuContent align="end" className="w-56"> type="button"
<DropdownMenuItem variant={viewMode === "table" ? "default" : "ghost"}
onClick={handleTemplateDownload} size="sm"
data-testid="tenant-template-menu-item" className="h-9 gap-1.5"
className="cursor-pointer" aria-pressed={viewMode === "table"}
onClick={() => setViewMode("table")}
data-testid="tenant-view-table-btn"
> >
<FileSpreadsheet size={16} className="mr-2 opacity-50" /> <List size={14} />
{t("ui.admin.tenants.csv_template", "템플릿 다운로드")} {t("ui.admin.tenants.view.table", "평면")}
</DropdownMenuItem> </Button>
<DropdownMenuSeparator /> </div>
<DropdownMenuItem
onClick={() => fileInputRef.current?.click()}
disabled={importMutation.isPending}
data-testid="tenant-import-menu-item"
className="cursor-pointer"
>
<Upload size={16} className="mr-2 opacity-50" />
{t("ui.admin.tenants.import", "CSV 가져오기")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => exportMutation.mutate(false)}
disabled={exportMutation.isPending}
data-testid="tenant-export-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_without_ids",
"UUID 제외 내보내기",
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => exportMutation.mutate(true)}
disabled={exportMutation.isPending}
data-testid="tenant-export-with-ids-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_with_ids",
"UUID 포함 내보내기",
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</RoleGuard>
<Button <Button
variant="outline" type="button"
onClick={() => query.refetch()} variant={scopeTenantId ? "default" : "outline"}
disabled={query.isFetching} size="sm"
className="w-9 px-0" className="h-9 gap-2"
title={t("ui.common.refresh", "새로고침")} onClick={() => setScopePickerOpen(true)}
> data-testid="tenant-scope-picker-btn"
<RefreshCw size={16} /> >
<span className="sr-only"> <Network size={16} />
{t("ui.common.refresh", "새로고침")} {selectedScopeTenant
</span> ? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
</Button> name: selectedScopeTenant.name,
<RoleGuard roles={["super_admin"]}> })
<Button asChild> : t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
<Link to="/tenants/new"> </Button>
<Plus size={16} /> {scopeTenantId ? (
{t("ui.admin.tenants.add", "테넌트 추가")} <Button
</Link> type="button"
</Button> variant="ghost"
</RoleGuard> size="sm"
</div> className="h-9"
onClick={() => setScopeTenantId("")}
data-testid="tenant-scope-clear-btn"
>
{t("ui.common.clear", "초기화")}
</Button>
) : null}
</>
}
actions={
<>
<RoleGuard roles={["super_admin"]}>
<input
ref={fileInputRef}
type="file"
accept=".csv,text/csv"
className="hidden"
data-testid="tenant-import-input"
onChange={handleImportFile}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
data-testid="tenant-data-mgmt-btn"
className="gap-2 h-9"
>
<LayoutDashboard size={16} />
{t("ui.admin.tenants.data_mgmt", "데이터 관리")}
<ChevronDown size={14} className="opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={handleTemplateDownload}
data-testid="tenant-template-menu-item"
className="cursor-pointer"
>
<FileSpreadsheet
size={16}
className="mr-2 opacity-50"
/>
{t(
"ui.admin.tenants.csv_template",
"템플릿 다운로드",
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => fileInputRef.current?.click()}
disabled={importMutation.isPending}
data-testid="tenant-import-menu-item"
className="cursor-pointer"
>
<Upload size={16} className="mr-2 opacity-50" />
{t("ui.admin.tenants.import", "CSV 가져오기")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => exportMutation.mutate(false)}
disabled={exportMutation.isPending}
data-testid="tenant-export-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_without_ids",
"UUID 제외 내보내기",
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => exportMutation.mutate(true)}
disabled={exportMutation.isPending}
data-testid="tenant-export-with-ids-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_with_ids",
"UUID 포함 내보내기",
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</RoleGuard>
<Button
variant="outline"
onClick={() => query.refetch()}
disabled={query.isFetching}
className="h-9 w-9 px-0"
title={t("ui.common.refresh", "새로고침")}
>
<RefreshCw size={16} />
<span className="sr-only">
{t("ui.common.refresh", "새로고침")}
</span>
</Button>
<RoleGuard roles={["super_admin"]}>
<Button asChild size="sm" className="h-9">
<Link to="/tenants/new">
<Plus size={16} />
{t("ui.admin.tenants.add", "테넌트 추가")}
</Link>
</Button>
</RoleGuard>
</>
}
/>
{importMessage ? ( {importMessage ? (
<div <div
className="rounded-md border border-border bg-secondary px-3 py-2 text-sm" className="rounded-md border border-border bg-secondary px-3 py-2 text-sm"
@@ -884,7 +898,7 @@ function TenantListPage() {
{importMessage} {importMessage}
</div> </div>
) : null} ) : null}
</> </div>
} }
/> />