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">
{t( <p className="leading-relaxed">
"ui.dev.clients.general.tenant_access.subtitle", {t(
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.", "ui.dev.clients.general.tenant_access.subtitle",
)} "허용된 테넌트만 이 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,81 +2350,168 @@ 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", <div className="space-y-3">
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.", <Label className="text-sm font-semibold">
)} {t(
</p> "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">
<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.selected_title",
"ui.dev.clients.general.tenant_access.picker_label", "허용 테넌트",
"허용 테넌트 추가", )}
)} </Label>
</Label> <div className="overflow-hidden rounded-md border border-border bg-background">
<TenantAccessPicker {allowedTenantIds.length > 0 ? (
disabled={isGeneralSettingsReadOnly || !tenantAccessRestricted} <div className="max-h-80 overflow-auto">
selectedCount={allowedTenantIds.length} <Table>
onSelectTenant={(selection) => <TableHeader className="sticky top-0 z-10 bg-muted/50">
handleSelectAllowedTenant(selection.id) <TableRow>
} <TableHead className="w-[34%] px-4 py-3 text-left font-bold">
/> {t(
</div> "ui.dev.clients.general.tenant_access.table.name",
"테넌트명",
<div className="space-y-3"> )}
<Label className="text-sm font-semibold"> </TableHead>
{t( <TableHead className="w-[18%] px-4 py-3 text-left font-bold">
"ui.dev.clients.general.tenant_access.selected_title", {t(
"허용 테넌트", "ui.dev.clients.general.tenant_access.table.slug",
)} "슬러그",
</Label> )}
<div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3"> </TableHead>
{tenantAccessRestricted && allowedTenantIds.length > 0 ? ( <TableHead className="px-4 py-3 text-left font-bold">
<div className="flex flex-wrap gap-2"> {t(
{selectedAllowedTenants.map((tenant) => ( "ui.dev.clients.general.tenant_access.table.id",
<AllowedTenantBadge "테넌트 ID",
key={tenant.id} )}
tenant={tenant} </TableHead>
onRemove={() => toggleAllowedTenant(tenant.id)} <TableHead className="w-[112px] px-4 py-3 text-right font-bold">
disabled={isGeneralSettingsReadOnly} {t(
/> "ui.dev.clients.general.tenant_access.table.actions",
))} "작업",
{allowedTenantIds )}
.filter( </TableHead>
(tenantId) => </TableRow>
!selectedAllowedTenants.some( </TableHeader>
(tenant) => tenant.id === tenantId, <TableBody>
), {selectedAllowedTenants.map((tenant) => (
) <TableRow
.map((tenantId) => ( key={tenant.id}
<AllowedTenantBadge data-testid={`allowed-tenant-${tenant.id}`}
key={tenantId} >
tenant={{ id: tenantId, name: tenantId }} <TableCell className="px-4 py-3 font-medium">
onRemove={() => toggleAllowedTenant(tenantId)} {tenant.name}
disabled={isGeneralSettingsReadOnly} </TableCell>
/> <TableCell className="px-4 py-3 text-muted-foreground">
))} {tenant.slug || "-"}
</div> </TableCell>
) : ( <TableCell className="px-4 py-3 font-mono text-xs text-muted-foreground">
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground"> <span className="break-all">{tenant.id}</span>
{tenantAccessRestricted </TableCell>
? t( <TableCell className="px-4 py-3 text-right">
"ui.dev.clients.general.tenant_access.selected_empty", <div className="inline-flex items-center gap-1.5">
"아직 선택된 테넌트가 없습니다.", <CopyButton
) aria-label="테넌트 UUID 복사"
: t( className="h-8 w-8"
"ui.dev.clients.general.tenant_access.disabled", data-testid={`allowed-tenant-copy-${tenant.id}`}
"제한 없음", size="icon"
)} value={tenant.id}
</div> 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> </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(