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 = ""