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, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../components/ui/card";
import { CopyButton } from "../../components/ui/copy-button";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label"; import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch"; import { Switch } from "../../components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast"; import { toast } from "../../components/ui/use-toast";
import type { import type {
@@ -55,7 +64,6 @@ import { cn } from "../../lib/utils";
import { fetchMe, type UserProfile } from "../auth/authApi"; import { fetchMe, type UserProfile } from "../auth/authApi";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate"; import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import { ClientDetailTabs } from "./ClientDetailTabs"; import { ClientDetailTabs } from "./ClientDetailTabs";
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
import { TenantAccessPicker } from "./components/TenantAccessPicker"; import { TenantAccessPicker } from "./components/TenantAccessPicker";
import { import {
claimDateTimeValueToInputString, claimDateTimeValueToInputString,
@@ -2298,12 +2306,20 @@ function ClientGeneralPage() {
"테넌트 접근 제한", "테넌트 접근 제한",
)} )}
</CardTitle> </CardTitle>
<CardDescription> <div className="text-sm text-muted-foreground">
<p className="leading-relaxed">
{t( {t(
"ui.dev.clients.general.tenant_access.subtitle", "ui.dev.clients.general.tenant_access.subtitle",
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.", "허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
)} )}
</CardDescription> </p>
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
</div>
</div> </div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3"> <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"> <div className="space-y-0.5 text-right">
@@ -2334,24 +2350,19 @@ function ClientGeneralPage() {
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-5"> <CardContent className="space-y-3">
<p className="text-sm text-muted-foreground"> {tenantAccessRestricted ? (
{t( <div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-sm font-semibold"> <Label className="text-sm font-semibold">
{t( {t(
"ui.dev.clients.general.tenant_access.picker_label", "ui.dev.clients.general.tenant_access.picker_label",
"허용 테넌트 추가", "허용 테넌트 추가",
)} )}{" "}
<span className="text-destructive">*</span>
</Label> </Label>
<TenantAccessPicker <TenantAccessPicker
disabled={isGeneralSettingsReadOnly || !tenantAccessRestricted} disabled={isGeneralSettingsReadOnly}
selectedCount={allowedTenantIds.length} selectedCount={allowedTenantIds.length}
onSelectTenant={(selection) => onSelectTenant={(selection) =>
handleSelectAllowedTenant(selection.id) handleSelectAllowedTenant(selection.id)
@@ -2366,16 +2377,78 @@ function ClientGeneralPage() {
"허용 테넌트", "허용 테넌트",
)} )}
</Label> </Label>
<div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3"> <div className="overflow-hidden rounded-md border border-border bg-background">
{tenantAccessRestricted && allowedTenantIds.length > 0 ? ( {allowedTenantIds.length > 0 ? (
<div className="flex flex-wrap gap-2"> <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) => ( {selectedAllowedTenants.map((tenant) => (
<AllowedTenantBadge <TableRow
key={tenant.id} key={tenant.id}
tenant={tenant} data-testid={`allowed-tenant-${tenant.id}`}
onRemove={() => toggleAllowedTenant(tenant.id)} >
disabled={isGeneralSettingsReadOnly} <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 {allowedTenantIds
.filter( .filter(
@@ -2385,30 +2458,60 @@ function ClientGeneralPage() {
), ),
) )
.map((tenantId) => ( .map((tenantId) => (
<AllowedTenantBadge <TableRow
key={tenantId} key={tenantId}
tenant={{ id: tenantId, name: tenantId }} data-testid={`allowed-tenant-${tenantId}`}
onRemove={() => toggleAllowedTenant(tenantId)} >
disabled={isGeneralSettingsReadOnly} <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>
) : ( ) : (
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground"> <div className="flex min-h-[99px] items-center px-4 py-3 text-sm text-muted-foreground">
{tenantAccessRestricted {t(
? t(
"ui.dev.clients.general.tenant_access.selected_empty", "ui.dev.clients.general.tenant_access.selected_empty",
"아직 선택된 테넌트가 없습니다.", "아직 선택된 테넌트가 없습니다.",
)
: t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)} )}
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
) : null}
</CardContent> </CardContent>
</Card> </Card>

View File

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