From c308d0a7d4e2482f9b223d095a2fa1d6b469992d Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 17 Jun 2026 15:49:54 +0900 Subject: [PATCH] =?UTF-8?q?devfront=20RP=20=EC=84=A4=EC=A0=95=20=ED=91=9C?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=ED=99=94=20=EB=B0=8F=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/core/i18n/loader.ts | 19 +- .../features/clients/ClientGeneralPage.tsx | 885 +++++++++--------- .../clients/components/SettingsTable.tsx | 130 +++ devfront/src/lib/i18n.test.ts | 48 + devfront/src/locales/en.toml | 5 + devfront/src/locales/ko.toml | 12 +- devfront/src/locales/template.toml | 5 + 7 files changed, 656 insertions(+), 448 deletions(-) create mode 100644 devfront/src/features/clients/components/SettingsTable.tsx diff --git a/common/core/i18n/loader.ts b/common/core/i18n/loader.ts index 7396445b..1ae75121 100644 --- a/common/core/i18n/loader.ts +++ b/common/core/i18n/loader.ts @@ -29,6 +29,23 @@ function mergeTomlObjects(base: TomlObject, override: TomlObject): TomlObject { return result; } +function setTomlValue( + target: TomlObject, + path: string[], + value: TomlValue, +): void { + let cursor: TomlObject = target; + for (let index = 0; index < path.length - 1; index += 1) { + const key = path[index]; + const current = cursor[key]; + if (!current || typeof current === "string") { + cursor[key] = {}; + } + cursor = cursor[key] as TomlObject; + } + cursor[path[path.length - 1]] = value; +} + function isSupportedLocale(value: string): value is Locale { return (SUPPORTED_LOCALES as readonly string[]).includes(value); } @@ -82,7 +99,7 @@ function parseToml(raw: string): TomlObject { cursor = cursor[section] as TomlObject; } - cursor[key] = value; + setTomlValue(cursor, key.split("."), value); } return root; diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index ccdfdf18..ca202477 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -30,14 +30,6 @@ 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 { @@ -64,6 +56,16 @@ import { cn } from "../../lib/utils"; import { fetchMe, type UserProfile } from "../auth/authApi"; import { useDeveloperAccessGate } from "../developer-access/developerAccessGate"; import { ClientDetailTabs } from "./ClientDetailTabs"; +import { + SettingsTable, + SettingsTableBody, + SettingsTableCell, + SettingsTableEmptyState, + SettingsTableHead, + SettingsTableHeader, + SettingsTableRow, + SettingsTableShell, +} from "./components/SettingsTable"; import { TenantAccessPicker } from "./components/TenantAccessPicker"; import { claimDateTimeValueToInputString, @@ -730,7 +732,8 @@ function ClientGeneralPage() { if ( restricted && !normalized.some( - (scope) => scope.name.trim() === "tenants" || scope.name.trim() === "tenant", + (scope) => + scope.name.trim() === "tenants" || scope.name.trim() === "tenant", ) ) { normalized.push(buildTenantScope(`tenants-${Date.now()}`)); @@ -2284,110 +2287,105 @@ function ClientGeneralPage() { -
- - + + + - - - - + - - - {scopes.map((s) => ( - - - - - - - ))} - {scopes.length === 0 && ( - - - + + + + updateScope(s.id, "description", e.target.value) + } + className="h-8 text-xs" + placeholder={t( + "ui.dev.clients.general.scopes.description_placeholder", + "권한에 대한 설명", + )} + disabled={s.locked || isGeneralSettingsReadOnly} + /> + + +
+ + updateScope(s.id, "mandatory", checked) + } + disabled={s.locked || isGeneralSettingsReadOnly} + /> +
+
+ + + + + )) + ) : ( + + {t( + "msg.dev.clients.general.scopes.empty", + "등록된 스코프가 없습니다.", + )} + )} - -
+ {t( "ui.dev.clients.general.scopes.table.name", "Scope Name", )} - + + {t( "ui.dev.clients.general.scopes.table.description", "Description", )} - + + {t( "ui.dev.clients.general.scopes.table.mandatory", "Mandatory", )} - + + {t("ui.dev.clients.general.scopes.table.delete", "Delete")} -
- - updateScope(s.id, "name", e.target.value) - } - className="h-8 font-mono text-xs" - placeholder={t( - "ui.dev.clients.general.scopes.name_placeholder", - "e.g. profile", - )} - disabled={s.locked || isGeneralSettingsReadOnly} - /> - - - updateScope(s.id, "description", e.target.value) - } - className="h-8 text-xs" - placeholder={t( - "ui.dev.clients.general.scopes.description_placeholder", - "권한에 대한 설명", - )} - disabled={s.locked || isGeneralSettingsReadOnly} - /> - -
- - updateScope(s.id, "mandatory", checked) + + + {scopes.length > 0 ? ( + scopes.map((s) => ( + + + + updateScope(s.id, "name", e.target.value) } + className="h-8 font-mono text-xs" + placeholder={t( + "ui.dev.clients.general.scopes.name_placeholder", + "e.g. profile", + )} disabled={s.locked || isGeneralSettingsReadOnly} /> -
-
- -
- {t( - "msg.dev.clients.general.scopes.empty", - "등록된 스코프가 없습니다.", - )} -
-
+ + + @@ -2472,54 +2470,56 @@ function ClientGeneralPage() { "허용 테넌트", )} -
- {allowedTenantIds.length > 0 ? ( -
- - - - - {t( - "ui.dev.clients.general.tenant_access.table.name", - "테넌트명", - )} - - - {t( - "ui.dev.clients.general.tenant_access.table.slug", - "슬러그", - )} - - - {t( - "ui.dev.clients.general.tenant_access.table.id", - "테넌트 ID", - )} - - - {t( - "ui.dev.clients.general.tenant_access.table.actions", - "작업", - )} - - - - + + + + + + {t( + "ui.dev.clients.general.tenant_access.table.name", + "테넌트명", + )} + + + {t( + "ui.dev.clients.general.tenant_access.table.slug", + "슬러그", + )} + + + {t( + "ui.dev.clients.general.tenant_access.table.id", + "테넌트 ID", + )} + + + {t( + "ui.dev.clients.general.tenant_access.table.actions", + "작업", + )} + + + + + {selectedAllowedTenants.length > 0 ? ( + <> {selectedAllowedTenants.map((tenant) => ( - - - {tenant.name} - - + + + {tenant.name} + + + {tenant.slug || "-"} - - + + {tenant.id} - - + +
-
-
+ + ))} {allowedTenantIds .filter( @@ -2553,20 +2553,22 @@ function ClientGeneralPage() { ), ) .map((tenantId) => ( - - - {tenantId} - - + + + {tenantId} + + + - - - + + {tenantId} - - + +
-
-
+ + ))} - -
-
- ) : ( -
- {t( - "ui.dev.clients.general.tenant_access.selected_empty", - "아직 선택된 테넌트가 없습니다.", + + ) : ( + + {t( + "ui.dev.clients.general.tenant_access.selected_empty", + "아직 선택된 테넌트가 없습니다.", + )} + )} -
- )} -
+ + + ) : null} @@ -2638,275 +2640,234 @@ function ClientGeneralPage() { -
+
-
- - + + + - - - - - - - - + - - - {idTokenClaims.map((claim) => { - const defaultValueError = - claimDefaultValueValidationError(claim); + + + {idTokenClaims.length > 0 ? ( + idTokenClaims.map((claim) => { + const defaultValueError = + claimDefaultValueValidationError(claim); - return ( - - - - - - - -
+ {t( "ui.dev.clients.general.id_token_claims.table.key", "Claim Key", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.namespace", "Namespace", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.value_type", "Value Type", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.nullable", "Nullable", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.read_user_allowed", "User read", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.write_user_allowed", "User write", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.default_value", "Default Value", )} - + + {t( "ui.dev.clients.general.id_token_claims.table.delete", "Delete", )} -
- - updateIdTokenClaim( - claim.id, - "key", - e.target.value, - ) - } - className="h-9 font-mono text-xs" - placeholder={t( - "ui.dev.clients.general.id_token_claims.key_placeholder", - "e.g. locale", - )} - disabled={isGeneralSettingsReadOnly} - /> - - - {t( - "ui.dev.clients.general.id_token_claims.namespace_rp_claims", - "rp_claims", - )} - - - - -
- - updateIdTokenClaim( - claim.id, - "nullable", - checked, - ) - } - aria-label={t( - "ui.dev.clients.general.id_token_claims.nullable_label", - "Nullable", - )} - disabled={isGeneralSettingsReadOnly} - /> -
-
-
- - setIdTokenClaimPermissionAllowed( - claim.id, - "readPermission", - checked, - ) - } - aria-label={t( - "ui.dev.clients.general.id_token_claims.read_user_allowed_label", - "사용자 읽기 허용", - )} - disabled={isGeneralSettingsReadOnly} - /> -
-
-
- - setIdTokenClaimPermissionAllowed( - claim.id, - "writePermission", - checked, - ) - } - aria-label={t( - "ui.dev.clients.general.id_token_claims.write_user_allowed_label", - "사용자 쓰기 허용", - )} - disabled={isGeneralSettingsReadOnly} - /> -
-
- {claim.valueType === "array" || - claim.valueType === "object" ? ( -