From b55ab7bc67f13a1cff76d91d0545258741bc04b6 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 20 May 2026 13:21:37 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EC=83=9D=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90=20=EA=B6=8C=ED=95=9C=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/clients/ClientsPage.tsx | 68 ++++++++++++++++--- .../clients/clientCreateAccess.test.ts | 55 +++++++++++++++ .../features/clients/clientCreateAccess.ts | 44 ++++++++++++ devfront/src/locales/en.toml | 3 + devfront/src/locales/ko.toml | 3 + devfront/src/locales/template.toml | 3 + 6 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 devfront/src/features/clients/clientCreateAccess.test.ts create mode 100644 devfront/src/features/clients/clientCreateAccess.ts diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 29b13ab8..27753102 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -60,6 +60,7 @@ import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { cn } from "../../lib/utils"; import { fetchMe } from "../auth/authApi"; +import { resolveClientCreateAccess } from "./clientCreateAccess"; import { ClientLogo } from "./components/ClientLogo"; type ClientSortKey = "application" | "id" | "type" | "status" | "createdAt"; @@ -96,7 +97,8 @@ function ClientsPage() { } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: hasAccessToken && role === "user", + enabled: + hasAccessToken && (role === "user" || role === "tenant_member"), }); const { data: tenants } = useQuery({ queryKey: ["myTenants"], @@ -109,15 +111,14 @@ function ClientsPage() { enabled: hasAccessToken, }); - const canCreateClient = - (role !== "user" && role !== "tenant_member") || - requestStatus?.status === "approved"; - const isDeveloperRequestPending = requestStatus?.status === "pending"; + const createAccessState = resolveClientCreateAccess({ + role, + requestStatus: requestStatus?.status, + }); + const canCreateClient = createAccessState === "can_create"; + const isDeveloperRequestPending = createAccessState === "pending"; const canRequestDeveloperAccess = - role === "user" && - !isLoadingRequest && - !canCreateClient && - !isDeveloperRequestPending; + createAccessState === "request_required" && !isLoadingRequest; const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); @@ -278,7 +279,54 @@ function ClientsPage() { {t("ui.dev.clients.new", "새 클라이언트")} - ) : null + ) : isDeveloperRequestPending ? ( +
+

+ {t( + "msg.dev.clients.create_pending_detail", + "개발자 권한 신청을 검토 중입니다. 승인되면 연동 앱을 추가할 수 있습니다.", + )} +

+ +
+ ) : canRequestDeveloperAccess ? ( +
+

+ {t( + "msg.dev.clients.create_requires_request", + "연동 앱을 생성할 권한이 없습니다.\n개발자 권한 신청을 요청한 뒤 승인 받아주세요.", + ).replaceAll("\\n", "\n")} +

+ +
+ ) : ( +
+

+ {t( + "msg.dev.clients.create_forbidden_detail", + "연동 앱을 생성할 권한이 없습니다. 관리자에게 개발자 권한 또는 적절한 RP 권한 부여를 요청해 주세요.", + )} +

+ +
+ ) } /> diff --git a/devfront/src/features/clients/clientCreateAccess.test.ts b/devfront/src/features/clients/clientCreateAccess.test.ts new file mode 100644 index 00000000..11ebcff8 --- /dev/null +++ b/devfront/src/features/clients/clientCreateAccess.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { resolveClientCreateAccess } from "./clientCreateAccess"; + +describe("client create access", () => { + it("allows privileged roles to create clients without developer request approval", () => { + expect( + resolveClientCreateAccess({ + role: "rp_admin", + }), + ).toBe("can_create"); + }); + + it("requires a developer request for basic users without approval", () => { + expect( + resolveClientCreateAccess({ + role: "user", + requestStatus: "none", + }), + ).toBe("request_required"); + }); + + it("shows pending state while a developer request is under review", () => { + expect( + resolveClientCreateAccess({ + role: "tenant_member", + requestStatus: "pending", + }), + ).toBe("pending"); + }); + + it("allows client creation after developer request approval", () => { + expect( + resolveClientCreateAccess({ + role: "user", + requestStatus: "approved", + }), + ).toBe("can_create"); + }); + + it("routes cancelled or rejected requests back to requestable state", () => { + expect( + resolveClientCreateAccess({ + role: "user", + requestStatus: "cancelled", + }), + ).toBe("request_required"); + + expect( + resolveClientCreateAccess({ + role: "user", + requestStatus: "rejected", + }), + ).toBe("request_required"); + }); +}); diff --git a/devfront/src/features/clients/clientCreateAccess.ts b/devfront/src/features/clients/clientCreateAccess.ts new file mode 100644 index 00000000..150dce0e --- /dev/null +++ b/devfront/src/features/clients/clientCreateAccess.ts @@ -0,0 +1,44 @@ +import type { DeveloperRequestStatus } from "../../lib/devApi"; + +export type ClientCreateAccessState = + | "can_create" + | "pending" + | "request_required" + | "forbidden"; + +type ResolveClientCreateAccessParams = { + role: string; + requestStatus?: DeveloperRequestStatus; +}; + +function canSelfRequestDeveloperAccess(role: string) { + return role === "user" || role === "tenant_member"; +} + +export function resolveClientCreateAccess({ + role, + requestStatus, +}: ResolveClientCreateAccessParams): ClientCreateAccessState { + if (!canSelfRequestDeveloperAccess(role)) { + return "can_create"; + } + + if (requestStatus === "approved") { + return "can_create"; + } + + if (requestStatus === "pending") { + return "pending"; + } + + if ( + requestStatus === "none" || + requestStatus === "rejected" || + requestStatus === "cancelled" || + typeof requestStatus === "undefined" + ) { + return "request_required"; + } + + return "forbidden"; +} diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index f7317f0e..79f5d67f 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -341,6 +341,9 @@ empty = "No RPs are available." empty_detail = "RPs will appear here when a relationship is assigned to your account." empty_can_create = "No linked apps have been registered yet." empty_can_create_detail = "Create a new RP with the Add linked app button, and it will appear here." +create_requires_request = "You do not have permission to create applications.\nSubmit a developer access request and wait for approval." +create_pending_detail = "Your developer access request is under review. You will be able to add applications after approval." +create_forbidden_detail = "You do not have permission to create applications. Ask an administrator to grant developer access or the appropriate RP permissions." empty_filtered = "No linked apps match the current filters." empty_filtered_detail = "Try changing the search text or filters." empty_pending = "Your developer access request is under review." diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 3ce007e4..accd5310 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -338,6 +338,9 @@ empty = "조회 가능한 RP가 없습니다." empty_detail = "RP 관계가 부여되면 이 목록에 해당 RP가 표시됩니다." empty_can_create = "아직 등록된 연동 앱이 없습니다." empty_can_create_detail = "연동 앱 추가 버튼으로 새 RP를 생성하면 이 목록에 표시됩니다." +create_requires_request = "연동 앱을 생성할 권한이 없습니다.\n개발자 권한 신청을 요청한 뒤 승인 받아주세요." +create_pending_detail = "개발자 권한 신청을 검토 중입니다. 승인되면 연동 앱을 추가할 수 있습니다." +create_forbidden_detail = "연동 앱을 생성할 권한이 없습니다. 관리자에게 개발자 권한 또는 적절한 RP 권한 부여를 요청해 주세요." empty_filtered = "조건에 맞는 연동 앱이 없습니다." empty_filtered_detail = "검색어나 필터 조건을 변경해 보세요." empty_pending = "개발자 권한 신청을 검토 중입니다." diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index d144c050..a0dbec89 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -379,6 +379,9 @@ empty = "" empty_detail = "" empty_can_create = "" empty_can_create_detail = "" +create_requires_request = "" +create_pending_detail = "" +create_forbidden_detail = "" empty_filtered = "" empty_filtered_detail = "" empty_pending = ""