1
0
forked from baron/baron-sso

허용 테넌트 테이블로 전환

This commit is contained in:
2026-06-15 13:37:08 +09:00
parent 11403b2151
commit 98dd924e9f
2 changed files with 183 additions and 80 deletions

View File

@@ -26,9 +26,18 @@ import {
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { CopyButton } from "../../components/ui/copy-button";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast";
import type {
@@ -55,7 +64,6 @@ import { cn } from "../../lib/utils";
import { fetchMe, type UserProfile } from "../auth/authApi";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import { ClientDetailTabs } from "./ClientDetailTabs";
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
import { TenantAccessPicker } from "./components/TenantAccessPicker";
import {
claimDateTimeValueToInputString,
@@ -2298,12 +2306,20 @@ function ClientGeneralPage() {
"테넌트 접근 제한",
)}
</CardTitle>
<CardDescription>
{t(
"ui.dev.clients.general.tenant_access.subtitle",
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
)}
</CardDescription>
<div className="text-sm text-muted-foreground">
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.subtitle",
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
)}
</p>
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
@@ -2334,81 +2350,168 @@ function ClientGeneralPage() {
</div>
</div>
</CardHeader>
<CardContent className="space-y-5">
<p className="text-sm text-muted-foreground">
{t(
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
<CardContent className="space-y-3">
{tenantAccessRestricted ? (
<div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.picker_label",
"허용 테넌트 추가",
)}{" "}
<span className="text-destructive">*</span>
</Label>
<TenantAccessPicker
disabled={isGeneralSettingsReadOnly}
selectedCount={allowedTenantIds.length}
onSelectTenant={(selection) =>
handleSelectAllowedTenant(selection.id)
}
/>
</div>
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.picker_label",
"허용 테넌트 추가",
)}
</Label>
<TenantAccessPicker
disabled={isGeneralSettingsReadOnly || !tenantAccessRestricted}
selectedCount={allowedTenantIds.length}
onSelectTenant={(selection) =>
handleSelectAllowedTenant(selection.id)
}
/>
</div>
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.selected_title",
"허용 테넌트",
)}
</Label>
<div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3">
{tenantAccessRestricted && allowedTenantIds.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedAllowedTenants.map((tenant) => (
<AllowedTenantBadge
key={tenant.id}
tenant={tenant}
onRemove={() => toggleAllowedTenant(tenant.id)}
disabled={isGeneralSettingsReadOnly}
/>
))}
{allowedTenantIds
.filter(
(tenantId) =>
!selectedAllowedTenants.some(
(tenant) => tenant.id === tenantId,
),
)
.map((tenantId) => (
<AllowedTenantBadge
key={tenantId}
tenant={{ id: tenantId, name: tenantId }}
onRemove={() => toggleAllowedTenant(tenantId)}
disabled={isGeneralSettingsReadOnly}
/>
))}
</div>
) : (
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">
{tenantAccessRestricted
? t(
"ui.dev.clients.general.tenant_access.selected_empty",
"아직 선택된 테넌트가 없습니다.",
)
: t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)}
</div>
)}
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.selected_title",
"허용 테넌트",
)}
</Label>
<div className="overflow-hidden rounded-md border border-border bg-background">
{allowedTenantIds.length > 0 ? (
<div className="max-h-80 overflow-auto">
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted/50">
<TableRow>
<TableHead className="w-[34%] px-4 py-3 text-left font-bold">
{t(
"ui.dev.clients.general.tenant_access.table.name",
"테넌트명",
)}
</TableHead>
<TableHead className="w-[18%] px-4 py-3 text-left font-bold">
{t(
"ui.dev.clients.general.tenant_access.table.slug",
"슬러그",
)}
</TableHead>
<TableHead className="px-4 py-3 text-left font-bold">
{t(
"ui.dev.clients.general.tenant_access.table.id",
"테넌트 ID",
)}
</TableHead>
<TableHead className="w-[112px] px-4 py-3 text-right font-bold">
{t(
"ui.dev.clients.general.tenant_access.table.actions",
"작업",
)}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedAllowedTenants.map((tenant) => (
<TableRow
key={tenant.id}
data-testid={`allowed-tenant-${tenant.id}`}
>
<TableCell className="px-4 py-3 font-medium">
{tenant.name}
</TableCell>
<TableCell className="px-4 py-3 text-muted-foreground">
{tenant.slug || "-"}
</TableCell>
<TableCell className="px-4 py-3 font-mono text-xs text-muted-foreground">
<span className="break-all">{tenant.id}</span>
</TableCell>
<TableCell className="px-4 py-3 text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenant.id}`}
size="icon"
value={tenant.id}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenant.id)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenant.id}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</TableCell>
</TableRow>
))}
{allowedTenantIds
.filter(
(tenantId) =>
!selectedAllowedTenants.some(
(tenant) => tenant.id === tenantId,
),
)
.map((tenantId) => (
<TableRow
key={tenantId}
data-testid={`allowed-tenant-${tenantId}`}
>
<TableCell className="px-4 py-3 font-medium">
{tenantId}
</TableCell>
<TableCell className="px-4 py-3 text-muted-foreground">
-
</TableCell>
<TableCell className="px-4 py-3 font-mono text-xs text-muted-foreground">
<span className="break-all">{tenantId}</span>
</TableCell>
<TableCell className="px-4 py-3 text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenantId}`}
size="icon"
value={tenantId}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenantId)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenantId}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex min-h-[99px] items-center px-4 py-3 text-sm text-muted-foreground">
{t(
"ui.dev.clients.general.tenant_access.selected_empty",
"아직 선택된 테넌트가 없습니다.",
)}
</div>
)}
</div>
</div>
</div>
</div>
) : null}
</CardContent>
</Card>

View File

@@ -133,7 +133,7 @@ export function TenantAccessPicker({
{selectedCount > 0
? t(
"msg.dev.clients.general.tenant_access.picker_hint_with_count",
"선택기를 열어 허용 테넌트를 추가하세요. 현재 {{count}}개가 선택되어 있습니다.",
"현재 {{count}}개가 선택되어 있습니다.",
{ count: selectedCount },
)
: t(